From 180930e44561f27ef551bdab5cd8fa2b1a3e66df Mon Sep 17 00:00:00 2001 From: Maksim Ivanov Date: Mon, 30 Oct 2023 10:02:23 +0300 Subject: [PATCH] feat(kit): add `InputMultiDate` (#5620) --- .../mobile-calendar-dialog.component.ts | 2 +- .../mobile-calendar.component.ts | 29 +- projects/demo/src/modules/app/app.routes.ts | 9 + projects/demo/src/modules/app/pages.ts | 8 + .../input-date-multi/examples/1/index.html | 9 + .../input-date-multi/examples/1/index.ts | 21 ++ .../examples/import/declare-form.md | 12 + .../examples/import/import-module.md | 14 + .../examples/import/insert-template.md | 10 + .../input-date-multi.component.ts | 80 +++++ .../input-date-multi.module.ts | 35 ++ .../input-date-multi.template.html | 181 ++++++++++ projects/kit/components/index.ts | 1 + .../kit/components/input-date-multi/index.ts | 2 + .../input-date-multi.component.ts | 314 ++++++++++++++++++ .../input-date-multi.module.ts | 39 +++ .../input-date-multi.template.html | 75 +++++ .../input-date-multi/ng-package.json | 8 + .../input-tag/input-tag.component.ts | 4 +- projects/kit/utils/date/index.ts | 1 + projects/kit/utils/date/ng-package.json | 5 + projects/kit/utils/date/update.ts | 10 + projects/kit/utils/index.ts | 1 + 23 files changed, 860 insertions(+), 10 deletions(-) create mode 100644 projects/demo/src/modules/components/input-date-multi/examples/1/index.html create mode 100644 projects/demo/src/modules/components/input-date-multi/examples/1/index.ts create mode 100644 projects/demo/src/modules/components/input-date-multi/examples/import/declare-form.md create mode 100644 projects/demo/src/modules/components/input-date-multi/examples/import/import-module.md create mode 100644 projects/demo/src/modules/components/input-date-multi/examples/import/insert-template.md create mode 100644 projects/demo/src/modules/components/input-date-multi/input-date-multi.component.ts create mode 100644 projects/demo/src/modules/components/input-date-multi/input-date-multi.module.ts create mode 100644 projects/demo/src/modules/components/input-date-multi/input-date-multi.template.html create mode 100644 projects/kit/components/input-date-multi/index.ts create mode 100644 projects/kit/components/input-date-multi/input-date-multi.component.ts create mode 100644 projects/kit/components/input-date-multi/input-date-multi.module.ts create mode 100644 projects/kit/components/input-date-multi/input-date-multi.template.html create mode 100644 projects/kit/components/input-date-multi/ng-package.json create mode 100644 projects/kit/utils/date/index.ts create mode 100644 projects/kit/utils/date/ng-package.json create mode 100644 projects/kit/utils/date/update.ts diff --git a/projects/addon-mobile/components/mobile-calendar-dialog/mobile-calendar-dialog.component.ts b/projects/addon-mobile/components/mobile-calendar-dialog/mobile-calendar-dialog.component.ts index ea10a4c11336..c0bd9cee8a61 100644 --- a/projects/addon-mobile/components/mobile-calendar-dialog/mobile-calendar-dialog.component.ts +++ b/projects/addon-mobile/components/mobile-calendar-dialog/mobile-calendar-dialog.component.ts @@ -21,7 +21,7 @@ export class TuiMobileCalendarDialogComponent { constructor( @Inject(POLYMORPHEUS_CONTEXT) readonly context: TuiDialogContext< - TuiDay | TuiDayRange, + TuiDay | TuiDayRange | readonly TuiDay[], TuiMobileCalendarData | undefined >, ) {} diff --git a/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts b/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts index bafb95e9e99f..8e4ba832a9a0 100644 --- a/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts +++ b/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts @@ -38,6 +38,7 @@ import { TUI_CANCEL_WORD, TUI_CHOOSE_DAY_OR_RANGE_TEXTS, TUI_DONE_WORD, + tuiImmutableUpdateInputDateMulti, } from '@taiga-ui/kit'; import {identity, MonoTypeOperatorFunction, Observable, race, timer} from 'rxjs'; import { @@ -98,9 +99,9 @@ export class TuiMobileCalendarComponent implements AfterViewInit { readonly cancel = new EventEmitter(); @Output() - readonly confirm = new EventEmitter(); + readonly confirm = new EventEmitter(); - value: TuiDay | TuiDayRange | null = null; + value: TuiDay | TuiDayRange | readonly TuiDay[] | null = null; readonly years = Array.from({length: RANGE}, (_, i) => i + STARTING_YEAR); @@ -167,11 +168,13 @@ export class TuiMobileCalendarComponent implements AfterViewInit { return; } - if ( - this.value === null || - this.value instanceof TuiDay || - !this.value.isSingleDay - ) { + if (!(this.value instanceof TuiDayRange) && !(this.value instanceof TuiDay)) { + this.value = tuiImmutableUpdateInputDateMulti(this.value ?? [], day); + + return; + } + + if (this.value instanceof TuiDay || !this.value?.isSingleDay) { this.value = new TuiDayRange(day, day); return; @@ -242,6 +245,10 @@ export class TuiMobileCalendarComponent implements AfterViewInit { return this.value.year; } + if (!(this.value instanceof TuiDayRange)) { + return this.value?.[0]?.year ?? this.today.year; + } + return this.value.from.year; } @@ -254,6 +261,14 @@ export class TuiMobileCalendarComponent implements AfterViewInit { return this.value.month + (this.value.year - STARTING_YEAR) * MONTHS_IN_YEAR; } + if (!(this.value instanceof TuiDayRange)) { + return ( + (this.value?.[0]?.month ?? this.today.month) + + ((this.value?.[0]?.year ?? this.today.year) - STARTING_YEAR) * + MONTHS_IN_YEAR + ); + } + return ( this.value.from.month + (this.value.from.year - STARTING_YEAR) * MONTHS_IN_YEAR diff --git a/projects/demo/src/modules/app/app.routes.ts b/projects/demo/src/modules/app/app.routes.ts index 32db166066bc..a64a3d707731 100644 --- a/projects/demo/src/modules/app/app.routes.ts +++ b/projects/demo/src/modules/app/app.routes.ts @@ -588,6 +588,15 @@ export const ROUTES: Routes = [ title: `InputDate`, }, }, + { + path: `components/input-date-multi`, + loadChildren: async () => + (await import(`../components/input-date-multi/input-date-multi.module`)) + .ExampleTuiInputDateMultiModule, + data: { + title: `InputDateMulti`, + }, + }, { path: `components/input-card`, loadChildren: async () => diff --git a/projects/demo/src/modules/app/pages.ts b/projects/demo/src/modules/app/pages.ts index 1a9631994122..95838a0c6e0b 100644 --- a/projects/demo/src/modules/app/pages.ts +++ b/projects/demo/src/modules/app/pages.ts @@ -412,6 +412,14 @@ export const pages: TuiDocPages = [ `неделя, месяц, год, дата, calendar`, route: `/components/input-date`, }, + { + section: `Components`, + title: `InputDateMulti`, + keywords: + `поле, инпут, форма, ввод, input, календарь, день, ` + + `неделя, месяц, год, дата, calendar, multiple`, + route: `/components/input-date-multi`, + }, { section: `Components`, title: `InputDateRange`, diff --git a/projects/demo/src/modules/components/input-date-multi/examples/1/index.html b/projects/demo/src/modules/components/input-date-multi/examples/1/index.html new file mode 100644 index 000000000000..e0131b1bf28b --- /dev/null +++ b/projects/demo/src/modules/components/input-date-multi/examples/1/index.html @@ -0,0 +1,9 @@ +
+ + Choose a dates + +
diff --git a/projects/demo/src/modules/components/input-date-multi/examples/1/index.ts b/projects/demo/src/modules/components/input-date-multi/examples/1/index.ts new file mode 100644 index 000000000000..3b95bb5ee7e5 --- /dev/null +++ b/projects/demo/src/modules/components/input-date-multi/examples/1/index.ts @@ -0,0 +1,21 @@ +import {Component} from '@angular/core'; +import {FormControl, FormGroup} from '@angular/forms'; +import {changeDetection} from '@demo/emulate/change-detection'; +import {encapsulation} from '@demo/emulate/encapsulation'; +import {TuiDay} from '@taiga-ui/cdk'; + +@Component({ + selector: 'tui-input-date-multi-example-1', + templateUrl: './index.html', + changeDetection, + encapsulation, +}) +export class TuiInputDateMultiExample1 { + readonly testForm = new FormGroup({ + testValue: new FormControl([ + new TuiDay(2017, 0, 7), + new TuiDay(2017, 0, 10), + new TuiDay(2017, 0, 15), + ]), + }); +} diff --git a/projects/demo/src/modules/components/input-date-multi/examples/import/declare-form.md b/projects/demo/src/modules/components/input-date-multi/examples/import/declare-form.md new file mode 100644 index 000000000000..d5bd0c688757 --- /dev/null +++ b/projects/demo/src/modules/components/input-date-multi/examples/import/declare-form.md @@ -0,0 +1,12 @@ +```ts +import {FormControl, FormGroup} from '@angular/forms'; + +@Component({ + // ... +}) +export class MyComponent { + testForm = new FormGroup({ + testValue: new FormControl([]), + }); +} +``` diff --git a/projects/demo/src/modules/components/input-date-multi/examples/import/import-module.md b/projects/demo/src/modules/components/input-date-multi/examples/import/import-module.md new file mode 100644 index 000000000000..6b4d20b27918 --- /dev/null +++ b/projects/demo/src/modules/components/input-date-multi/examples/import/import-module.md @@ -0,0 +1,14 @@ +```ts +import {ReactiveFormsModule} from '@angular/forms'; +import {TuiInputDateMultiModule} from '@taiga-ui/kit'; + +@NgModule({ + imports: [ + // ... + ReactiveFormsModule, + TuiInputDateMultiModule, + ], + // ... +}) +export class MyModule {} +``` diff --git a/projects/demo/src/modules/components/input-date-multi/examples/import/insert-template.md b/projects/demo/src/modules/components/input-date-multi/examples/import/insert-template.md new file mode 100644 index 000000000000..c62143839c67 --- /dev/null +++ b/projects/demo/src/modules/components/input-date-multi/examples/import/insert-template.md @@ -0,0 +1,10 @@ +```html +
+ + Choose a date + +
+``` diff --git a/projects/demo/src/modules/components/input-date-multi/input-date-multi.component.ts b/projects/demo/src/modules/components/input-date-multi/input-date-multi.component.ts new file mode 100644 index 000000000000..1cc1aca702be --- /dev/null +++ b/projects/demo/src/modules/components/input-date-multi/input-date-multi.component.ts @@ -0,0 +1,80 @@ +import {Component, forwardRef} from '@angular/core'; +import {FormControl, Validators} from '@angular/forms'; +import {changeDetection} from '@demo/emulate/change-detection'; +import {TuiDocExample} from '@taiga-ui/addon-doc'; +import { + ALWAYS_FALSE_HANDLER, + TUI_FIRST_DAY, + TUI_LAST_DAY, + TuiBooleanHandler, + TuiDay, +} from '@taiga-ui/cdk'; +import {TUI_DEFAULT_MARKER_HANDLER, TuiMarkerHandler} from '@taiga-ui/core'; + +import {AbstractExampleTuiControl} from '../abstract/control'; +import {ABSTRACT_PROPS_ACCESSOR} from '../abstract/inherited-documentation/abstract-props-accessor'; + +@Component({ + selector: 'example-tui-input-date-multi', + templateUrl: './input-date-multi.template.html', + changeDetection, + providers: [ + { + provide: ABSTRACT_PROPS_ACCESSOR, + useExisting: forwardRef(() => ExampleTuiInputDateMultiComponent), + }, + ], +}) +export class ExampleTuiInputDateMultiComponent extends AbstractExampleTuiControl { + readonly exampleForm = import('./examples/import/declare-form.md?raw'); + readonly exampleModule = import('./examples/import/import-module.md?raw'); + readonly exampleHtml = import('./examples/import/insert-template.md?raw'); + + readonly example1: TuiDocExample = { + TypeScript: import('./examples/1/index.ts?raw'), + HTML: import('./examples/1/index.html?raw'), + }; + + minVariants = [ + TUI_FIRST_DAY, + new TuiDay(2017, 2, 5), + new TuiDay(1900, 0, 1), + new TuiDay(new Date().getFullYear() + 3, 1, 1), + ]; + + min = this.minVariants[0]; + + maxVariants = [ + TUI_LAST_DAY, + new TuiDay(2017, 11, 11), + new TuiDay(2020, 2, 5), + new TuiDay(2300, 0, 1), + ]; + + max = this.maxVariants[0]; + + rowsVariants = [Infinity, 10, 3, 2]; + + rows = this.rowsVariants[0]; + + readonly disabledItemHandlerVariants: ReadonlyArray> = [ + ALWAYS_FALSE_HANDLER, + ({day}) => day % 3 === 0, + ]; + + disabledItemHandler = this.disabledItemHandlerVariants[0]; + + readonly markerHandlerVariants: readonly TuiMarkerHandler[] = [ + TUI_DEFAULT_MARKER_HANDLER, + (day: TuiDay) => + day.day % 2 === 0 + ? ['var(--tui-primary)', 'var(--tui-info-fill)'] + : ['var(--tui-success-fill)'], + ]; + + markerHandler: TuiMarkerHandler = this.markerHandlerVariants[0]; + + expandable = false; + + control = new FormControl([], Validators.required); +} diff --git a/projects/demo/src/modules/components/input-date-multi/input-date-multi.module.ts b/projects/demo/src/modules/components/input-date-multi/input-date-multi.module.ts new file mode 100644 index 000000000000..d20a43e0f98c --- /dev/null +++ b/projects/demo/src/modules/components/input-date-multi/input-date-multi.module.ts @@ -0,0 +1,35 @@ +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {ReactiveFormsModule} from '@angular/forms'; +import {TuiAddonDocModule, tuiGetDocModules} from '@taiga-ui/addon-doc'; +import {TuiMobileCalendarDialogModule} from '@taiga-ui/addon-mobile'; +import { + TuiDropdownModule, + TuiHintModule, + TuiLinkModule, + TuiTextfieldControllerModule, +} from '@taiga-ui/core'; +import {TuiInputDateMultiModule} from '@taiga-ui/kit'; + +import {InheritedDocumentationModule} from '../abstract/inherited-documentation/inherited-documentation.module'; +import {TuiInputDateMultiExample1} from './examples/1'; +import {ExampleTuiInputDateMultiComponent} from './input-date-multi.component'; + +@NgModule({ + imports: [ + CommonModule, + TuiLinkModule, + TuiHintModule, + TuiDropdownModule, + TuiAddonDocModule, + ReactiveFormsModule, + TuiInputDateMultiModule, + TuiMobileCalendarDialogModule, + TuiTextfieldControllerModule, + InheritedDocumentationModule, + tuiGetDocModules(ExampleTuiInputDateMultiComponent), + ], + declarations: [ExampleTuiInputDateMultiComponent, TuiInputDateMultiExample1], + exports: [ExampleTuiInputDateMultiComponent], +}) +export class ExampleTuiInputDateMultiModule {} diff --git a/projects/demo/src/modules/components/input-date-multi/input-date-multi.template.html b/projects/demo/src/modules/components/input-date-multi/input-date-multi.template.html new file mode 100644 index 000000000000..4a484576fcee --- /dev/null +++ b/projects/demo/src/modules/components/input-date-multi/input-date-multi.template.html @@ -0,0 +1,181 @@ + + + + + + + + + + + + Choose a date + + + + + + Disabled state (use + formControl.disable() + ) + + + Expandable + + +
A handler that gets a date and returns true if it is disabled.
+ + Must be a pure function +
+ + A handler that gets date and returns null or a tuple with circled marker colors + + + Minimum date + + + Maximum date + + + A number of visible rows in + expandable + mode + +
+ +
+ + +

+ Mobile calendar does not use the same dropdown with calendar as desktop uses. It uses digital keyboard. If + you want to open + + mobile calendar + + , add imports of + TuiMobileCalendarDialogModule + and + TuiDialogModule + into your root module. Also, check that you did not forget about + + tui-root + +

+ +
    +
  1. +

    + Import an Angular module for forms and + TuiInputDateMultiModule + in the same module where you want to use our component: +

    + + +
  2. + +
  3. +

    + Declare a form ( + FormGroup + ) or a control ( + FormControl + ) in your component: +

    + + +
  4. + +
  5. + Use + tui-input-date[multiple] + in template: + + +
  6. +
+
+
diff --git a/projects/kit/components/index.ts b/projects/kit/components/index.ts index 645c13f2b755..fa89cfd4513f 100644 --- a/projects/kit/components/index.ts +++ b/projects/kit/components/index.ts @@ -20,6 +20,7 @@ export * from '@taiga-ui/kit/components/input'; export * from '@taiga-ui/kit/components/input-copy'; export * from '@taiga-ui/kit/components/input-count'; export * from '@taiga-ui/kit/components/input-date'; +export * from '@taiga-ui/kit/components/input-date-multi'; export * from '@taiga-ui/kit/components/input-date-range'; export * from '@taiga-ui/kit/components/input-date-time'; export * from '@taiga-ui/kit/components/input-files'; diff --git a/projects/kit/components/input-date-multi/index.ts b/projects/kit/components/input-date-multi/index.ts new file mode 100644 index 000000000000..f2d30fda447c --- /dev/null +++ b/projects/kit/components/input-date-multi/index.ts @@ -0,0 +1,2 @@ +export * from './input-date-multi.component'; +export * from './input-date-multi.module'; diff --git a/projects/kit/components/input-date-multi/input-date-multi.component.ts b/projects/kit/components/input-date-multi/input-date-multi.component.ts new file mode 100644 index 000000000000..ed5c54fbdbf6 --- /dev/null +++ b/projects/kit/components/input-date-multi/input-date-multi.component.ts @@ -0,0 +1,314 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + HostBinding, + HostListener, + Inject, + Injector, + Input, + Optional, + Self, + Type, + ViewChild, +} from '@angular/core'; +import {NgControl} from '@angular/forms'; +import {MaskitoOptions} from '@maskito/core'; +import {maskitoDateOptionsGenerator} from '@maskito/kit'; +import { + AbstractTuiMultipleControl, + AbstractTuiValueTransformer, + ALWAYS_FALSE_HANDLER, + changeDateSeparator, + TUI_DATE_FORMAT, + TUI_DATE_SEPARATOR, + TUI_IS_MOBILE, + tuiAsControl, + tuiAsFocusableItemAccessor, + TuiBooleanHandler, + tuiDateClamp, + TuiDateMode, + TuiDay, + TuiFocusableElementAccessor, + tuiIsString, + TuiMapper, + TuiMonth, +} from '@taiga-ui/cdk'; +import { + TUI_DEFAULT_MARKER_HANDLER, + TUI_TEXTFIELD_SIZE, + TuiDialogService, + TuiMarkerHandler, + TuiPrimitiveTextfieldComponent, + TuiSizeL, + TuiSizeS, + TuiTextfieldSizeDirective, + TuiWithOptionalMinMax, +} from '@taiga-ui/core'; +import {TuiStringifiableItem} from '@taiga-ui/kit/classes'; +import {TuiInputTagComponent} from '@taiga-ui/kit/components/input-tag'; +import { + TUI_DATE_TEXTS, + TUI_DATE_VALUE_TRANSFORMER, + TUI_DONE_WORD, + TUI_INPUT_DATE_OPTIONS, + TUI_MOBILE_CALENDAR, + tuiDateStreamWithTransformer, + TuiInputDateOptions, +} from '@taiga-ui/kit/tokens'; +import {tuiImmutableUpdateInputDateMulti} from '@taiga-ui/kit/utils'; +import {PolymorpheusComponent} from '@tinkoff/ng-polymorpheus'; +import {Observable} from 'rxjs'; +import {map, takeUntil} from 'rxjs/operators'; + +@Component({ + selector: 'tui-input-date[multiple]', + templateUrl: './input-date-multi.template.html', + styleUrls: ['../input-date/input-date.style.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + tuiAsFocusableItemAccessor(TuiInputDateMultiComponent), + tuiAsControl(TuiInputDateMultiComponent), + tuiDateStreamWithTransformer(TUI_DATE_VALUE_TRANSFORMER), + ], +}) +export class TuiInputDateMultiComponent + extends AbstractTuiMultipleControl + implements TuiWithOptionalMinMax, TuiFocusableElementAccessor +{ + @ViewChild(TuiPrimitiveTextfieldComponent) + private readonly textfield?: TuiPrimitiveTextfieldComponent; + + @ViewChild(TuiInputTagComponent) + private readonly inputTag?: TuiInputTagComponent; + + private month: TuiMonth | null = null; + + @Input() + min: TuiDay | null = this.options.min; + + @Input() + max: TuiDay | null = this.options.max; + + @Input() + disabledItemHandler: TuiBooleanHandler = ALWAYS_FALSE_HANDLER; + + @Input() + markerHandler: TuiMarkerHandler = TUI_DEFAULT_MARKER_HANDLER; + + @Input() + defaultActiveYearMonth = TuiMonth.currentLocal(); + + @Input() + expandable = false; + + @Input() + inputHidden = false; + + @Input() + @HostBinding('class._editable') + editable = true; + + @Input() + search: string | null = ''; + + @Input() + placeholder = ''; + + @Input() + rows = Infinity; + + maskitoOptions: MaskitoOptions = maskitoDateOptionsGenerator({ + mode: 'dd/mm/yyyy', + separator: '.', + min: this.min?.toLocalNativeDate(), + max: this.max?.toLocalNativeDate(), + }); + + open = false; + + readonly filler$: Observable = this.dateTexts$.pipe( + map(dateTexts => + changeDateSeparator(dateTexts[this.dateFormat], this.dateSeparator), + ), + ); + + constructor( + @Optional() + @Self() + @Inject(NgControl) + control: NgControl | null, + @Inject(ChangeDetectorRef) cdr: ChangeDetectorRef, + @Inject(Injector) private readonly injector: Injector, + @Inject(TUI_IS_MOBILE) readonly isMobile: boolean, + @Inject(TuiDialogService) private readonly dialogs: TuiDialogService, + @Optional() + @Inject(TUI_MOBILE_CALENDAR) + private readonly mobileCalendar: Type> | null, + @Inject(TUI_DATE_FORMAT) readonly dateFormat: TuiDateMode, + @Inject(TUI_DATE_SEPARATOR) readonly dateSeparator: string, + @Inject(TUI_DATE_TEXTS) + readonly dateTexts$: Observable>, + @Optional() + @Inject(TUI_DATE_VALUE_TRANSFORMER) + override readonly valueTransformer: AbstractTuiValueTransformer< + readonly TuiDay[] + > | null, + @Inject(TUI_INPUT_DATE_OPTIONS) private readonly options: TuiInputDateOptions, + @Inject(TUI_TEXTFIELD_SIZE) + private readonly textfieldSize: TuiTextfieldSizeDirective, + @Inject(TUI_DONE_WORD) readonly doneWord$: Observable, + ) { + super(control, cdr, valueTransformer); + } + + @Input() + tagValidator: TuiBooleanHandler = (tag: TuiDay | string) => { + const {year, month, day} = tuiIsString(tag) + ? TuiDay.parseRawDateString(tag) + : tag; + const date = new TuiDay(year, month, day); + + return ( + (TuiDay.isValidDay(year, month, day) && + this.min?.dayBefore(date) && + this.max?.dayAfter(date)) ?? + false + ); + }; + + @HostListener('click') + onClick(): void { + if (!this.isMobile) { + this.open = !this.open; + } + } + + readonly disabledItemHandlerWrapper: TuiMapper< + TuiBooleanHandler | TuiBooleanHandler, + TuiBooleanHandler | string> + > = handler => stringifiable => + tuiIsString(stringifiable) || handler(stringifiable.item); + + @HostBinding('attr.data-size') + get size(): TuiSizeL | TuiSizeS { + return this.textfieldSize.size; + } + + get nativeDropdownMode(): boolean { + return this.isMobile && !this.editable; + } + + get computedMin(): TuiDay { + return this.min ?? this.options.min; + } + + get computedMax(): TuiDay { + return this.max ?? this.options.max; + } + + get nativeFocusableElement(): HTMLInputElement | null { + return this.textfield?.nativeFocusableElement || null; + } + + get focused(): boolean { + return !!this.textfield?.focused; + } + + get computedMobile(): boolean { + return this.isMobile && !!this.mobileCalendar; + } + + get calendarIcon(): TuiInputDateOptions['icon'] { + return this.options.icon; + } + + get computedActiveYearMonth(): TuiMonth { + return ( + this.month || + this.value[this.value.length - 1] || + tuiDateClamp(this.defaultActiveYearMonth, this.computedMin, this.computedMax) + ); + } + + get canOpen(): boolean { + return this.interactive && !this.computedMobile; + } + + onIconClick(): void { + if (!this.computedMobile || !this.mobileCalendar) { + return; + } + + this.dialogs + .open( + new PolymorpheusComponent(this.mobileCalendar, this.injector), + { + size: 'fullscreen', + closeable: false, + data: { + single: false, + min: this.min, + max: this.max, + disabledItemHandler: this.disabledItemHandler, + }, + }, + ) + .pipe(takeUntil(this.destroy$)) + .subscribe(value => { + this.value = value; + }); + } + + onEnter(search: string): void { + if (!this.tagValidator(search)) { + return; + } + + this.value = tuiImmutableUpdateInputDateMulti( + this.value, + TuiDay.normalizeParse(search), + ); + + if (this.inputTag) { + this.inputTag.search = ''; + } + + this.done(); + } + + onValueChange(value: readonly TuiDay[]): void { + this.control?.updateValueAndValidity({emitEvent: false}); + + if (!value.length) { + this.onOpenChange(true); + } + + this.value = value; + } + + onDayClick(value: TuiDay): void { + this.value = tuiImmutableUpdateInputDateMulti(this.value, value); + } + + done(): void { + this.open = false; + } + + onMonthChange(month: TuiMonth): void { + this.month = month; + } + + onOpenChange(open: boolean): void { + this.open = open; + } + + onFocused(focused: boolean): void { + this.updateFocused(focused); + } + + override setDisabledState(): void { + super.setDisabledState(); + this.open = false; + } +} diff --git a/projects/kit/components/input-date-multi/input-date-multi.module.ts b/projects/kit/components/input-date-multi/input-date-multi.module.ts new file mode 100644 index 000000000000..bcea8042f715 --- /dev/null +++ b/projects/kit/components/input-date-multi/input-date-multi.module.ts @@ -0,0 +1,39 @@ +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {MaskitoModule} from '@maskito/angular'; +import {TuiMapperPipeModule} from '@taiga-ui/cdk'; +import { + TuiCalendarModule, + TuiHostedDropdownModule, + TuiLinkModule, + TuiPrimitiveTextfieldModule, + TuiSvgModule, + TuiTextfieldControllerModule, + TuiWrapperModule, +} from '@taiga-ui/core'; +import {TuiInputTagModule} from '@taiga-ui/kit/components/input-tag'; +import {PolymorpheusModule} from '@tinkoff/ng-polymorpheus'; + +import {TuiInputDateMultiComponent} from './input-date-multi.component'; + +@NgModule({ + imports: [ + CommonModule, + MaskitoModule, + PolymorpheusModule, + TuiWrapperModule, + TuiCalendarModule, + TuiSvgModule, + TuiLinkModule, + TuiInputTagModule, + FormsModule, + TuiMapperPipeModule, + TuiHostedDropdownModule, + TuiTextfieldControllerModule, + TuiPrimitiveTextfieldModule, + ], + declarations: [TuiInputDateMultiComponent], + exports: [TuiInputDateMultiComponent], +}) +export class TuiInputDateMultiModule {} diff --git a/projects/kit/components/input-date-multi/input-date-multi.template.html b/projects/kit/components/input-date-multi/input-date-multi.template.html new file mode 100644 index 000000000000..6499b75ade39 --- /dev/null +++ b/projects/kit/components/input-date-multi/input-date-multi.template.html @@ -0,0 +1,75 @@ + + + + + + + + + + + + +
+ +
+
+
diff --git a/projects/kit/components/input-date-multi/ng-package.json b/projects/kit/components/input-date-multi/ng-package.json new file mode 100644 index 000000000000..bab5ebcdb74a --- /dev/null +++ b/projects/kit/components/input-date-multi/ng-package.json @@ -0,0 +1,8 @@ +{ + "lib": { + "entryFile": "index.ts", + "styleIncludePaths": [ + "../../../core/styles" + ] + } +} diff --git a/projects/kit/components/input-tag/input-tag.component.ts b/projects/kit/components/input-tag/input-tag.component.ts index c716b7b7f56c..02c4b870d094 100644 --- a/projects/kit/components/input-tag/input-tag.component.ts +++ b/projects/kit/components/input-tag/input-tag.component.ts @@ -115,7 +115,7 @@ export class TuiInputTagComponent separator: RegExp | string = this.options.separator; @Input() - search = ''; + search: string | null = ''; @Input() editable = true; @@ -504,7 +504,7 @@ export class TuiInputTagComponent } private addTag(): void { - const inputValue = this.search.trim(); + const inputValue = this.search?.trim() ?? ''; if (!inputValue || this.disabledItemHandler(inputValue)) { return; diff --git a/projects/kit/utils/date/index.ts b/projects/kit/utils/date/index.ts new file mode 100644 index 000000000000..c37c258c7c13 --- /dev/null +++ b/projects/kit/utils/date/index.ts @@ -0,0 +1 @@ +export * from './update'; diff --git a/projects/kit/utils/date/ng-package.json b/projects/kit/utils/date/ng-package.json new file mode 100644 index 000000000000..bebf62dcb5e5 --- /dev/null +++ b/projects/kit/utils/date/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} diff --git a/projects/kit/utils/date/update.ts b/projects/kit/utils/date/update.ts new file mode 100644 index 000000000000..ddc5ef92b760 --- /dev/null +++ b/projects/kit/utils/date/update.ts @@ -0,0 +1,10 @@ +import {TuiDay} from '@taiga-ui/cdk'; + +export function tuiImmutableUpdateInputDateMulti( + value: readonly TuiDay[], + day: TuiDay, +): readonly TuiDay[] { + return value.find(item => item.daySame(day)) + ? value.filter(item => !item.daySame(day)) + : value.concat(day); +} diff --git a/projects/kit/utils/index.ts b/projects/kit/utils/index.ts index f51f1fe36d30..f5a94ead1357 100644 --- a/projects/kit/utils/index.ts +++ b/projects/kit/utils/index.ts @@ -1,3 +1,4 @@ +export * from '@taiga-ui/kit/utils/date'; export * from '@taiga-ui/kit/utils/files'; export * from '@taiga-ui/kit/utils/format'; export * from '@taiga-ui/kit/utils/mask';