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 ea10a4c11336a..c0bd9cee8a610 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 771459d8afb21..a9f15447073ec 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 7e9d016c86412..b8b21abc6c3cd 100644 --- a/projects/demo/src/modules/app/app.routes.ts +++ b/projects/demo/src/modules/app/app.routes.ts @@ -555,6 +555,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 8793dade914bc..e94f43dd479ff 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/abstract/multi.ts b/projects/demo/src/modules/components/abstract/multi.ts new file mode 100644 index 0000000000000..785de7f8ecc2a --- /dev/null +++ b/projects/demo/src/modules/components/abstract/multi.ts @@ -0,0 +1,24 @@ +import {FormControl, Validators} from '@angular/forms'; +import {TuiSizeL, TuiSizeS} from '@taiga-ui/core'; + +import {AbstractExampleTuiControl} from './control'; + +export abstract class AbstractExampleTuiMulti extends AbstractExampleTuiControl { + readonly control = new FormControl([], Validators.required); + + override readonly sizeVariants: ReadonlyArray = [`s`, `m`, `l`]; + override size: TuiSizeL | TuiSizeS = this.sizeVariants[this.sizeVariants.length - 1]; + + readonly rowsVariants = [100, 10, 3, 2]; + rows = this.rowsVariants[0]; + + expandable = false; + + editable = true; + + uniqueTags = true; + + inputHidden = false; + + search: string | null = ``; +} 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 0000000000000..e0131b1bf28bc --- /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 0000000000000..3b95bb5ee7e5c --- /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 0000000000000..d5bd0c6887575 --- /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 0000000000000..8cdbc4a3936f4 --- /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 {TuiInputDateModule} from '@taiga-ui/kit'; + +@NgModule({ + imports: [ + // ... + ReactiveFormsModule, + TuiInputDateModule, + ], + // ... +}) +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 0000000000000..1e5aa6ec75e43 --- /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 0000000000000..7aa9a4234ede8 --- /dev/null +++ b/projects/demo/src/modules/components/input-date-multi/input-date-multi.component.ts @@ -0,0 +1,78 @@ +import {Component, forwardRef} from '@angular/core'; +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 {TuiNamedDay} from '@taiga-ui/kit'; + +import {ABSTRACT_PROPS_ACCESSOR} from '../abstract/inherited-documentation/abstract-props-accessor'; +import {AbstractExampleTuiMulti} from '../abstract/multi'; + +const TWO_DOTS: [string, string] = ['var(--tui-primary)', 'var(--tui-info-fill)']; +const ONE_DOT: [string] = ['var(--tui-success-fill)']; + +@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 AbstractExampleTuiMulti { + 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]; + + readonly disabledItemHandlerVariants: ReadonlyArray> = [ + ALWAYS_FALSE_HANDLER, + ]; + + disabledItemHandler = this.disabledItemHandlerVariants[0]; + + readonly itemsVariants = [ + [], + [new TuiNamedDay(TUI_LAST_DAY.append({year: -1}), 'Until today')], + ]; + + readonly markerHandlerVariants: readonly TuiMarkerHandler[] = [ + TUI_DEFAULT_MARKER_HANDLER, + (day: TuiDay) => (day.day % 2 === 0 ? TWO_DOTS : ONE_DOT), + ]; + + markerHandler: TuiMarkerHandler = this.markerHandlerVariants[0]; + + items = this.itemsVariants[0]; +} 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 0000000000000..9c0e9b208912f --- /dev/null +++ b/projects/demo/src/modules/components/input-date-multi/input-date-multi.module.ts @@ -0,0 +1,31 @@ +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {ReactiveFormsModule} from '@angular/forms'; +import {TuiAddonDocModule, tuiGetDocModules} from '@taiga-ui/addon-doc'; +import { + TuiDropdownModule, + TuiHintModule, + TuiTextfieldControllerModule, +} from '@taiga-ui/core'; +import {TuiInputDateModule} 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, + ReactiveFormsModule, + TuiInputDateModule, + TuiHintModule, + TuiDropdownModule, + TuiAddonDocModule, + 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 0000000000000..732c6e0549a9e --- /dev/null +++ b/projects/demo/src/modules/components/input-date-multi/input-date-multi.template.html @@ -0,0 +1,196 @@ + + + + + + + + + + + + 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 + +
+ + + + Custom align content by text-align + + +
+ + +

+ 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 + TuiInputDateModule + 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 + TuiInputDate[multiple] + in template: + + +
  6. +
+
+
diff --git a/projects/demo/src/modules/components/input-tag/input-tag.component.ts b/projects/demo/src/modules/components/input-tag/input-tag.component.ts index 4e8b449b03dbe..bb53d7089f9f6 100644 --- a/projects/demo/src/modules/components/input-tag/input-tag.component.ts +++ b/projects/demo/src/modules/components/input-tag/input-tag.component.ts @@ -7,11 +7,10 @@ import { ALWAYS_TRUE_HANDLER, TuiBooleanHandler, } from '@taiga-ui/cdk'; -import {TuiSizeL, TuiSizeS} from '@taiga-ui/core'; import {TuiStringifiableItem} from '@taiga-ui/kit'; -import {AbstractExampleTuiControl} from '../abstract/control'; import {ABSTRACT_PROPS_ACCESSOR} from '../abstract/inherited-documentation/abstract-props-accessor'; +import {AbstractExampleTuiMulti} from '../abstract/multi'; @Component({ selector: 'example-input-tag', @@ -29,7 +28,7 @@ import {ABSTRACT_PROPS_ACCESSOR} from '../abstract/inherited-documentation/abstr ]), ], }) -export class ExampleTuiInputTagComponent extends AbstractExampleTuiControl { +export class ExampleTuiInputTagComponent extends AbstractExampleTuiMulti { readonly exampleModule = import('./examples/import/import-module.md?raw'); readonly exampleHtml = import('./examples/import/insert-template.md?raw'); @@ -81,17 +80,11 @@ export class ExampleTuiInputTagComponent extends AbstractExampleTuiControl { HTML: import('./examples/9/index.html?raw'), }; - readonly control = new FormControl( + override readonly control = new FormControl( ['John Cleese', 'Eric Idle', 'Michael Palin'], Validators.required, ); - editable = true; - - expandable = true; - - uniqueTags = true; - readonly separatorVariants = [',', ';', /[\d]/, /[\s,]/]; separator = this.separatorVariants[0]; @@ -104,14 +97,6 @@ export class ExampleTuiInputTagComponent extends AbstractExampleTuiControl { override maxLength: number | null = null; - search = ''; - - rows = 100; - - override readonly sizeVariants: ReadonlyArray = ['s', 'm', 'l']; - - override size: TuiSizeL | TuiSizeS = this.sizeVariants[this.sizeVariants.length - 1]; - tagValidatorVariants: ReadonlyArray> = [ ALWAYS_TRUE_HANDLER, item => item === 'test', @@ -120,8 +105,6 @@ export class ExampleTuiInputTagComponent extends AbstractExampleTuiControl { tagValidator = this.tagValidatorVariants[0]; - inputHidden = false; - readonly disabledItemHandlerVariants: Array< TuiBooleanHandler | string> > = [ALWAYS_FALSE_HANDLER, item => String(item).startsWith('T')]; diff --git a/projects/demo/src/modules/components/multi-select/multi-select.component.ts b/projects/demo/src/modules/components/multi-select/multi-select.component.ts index d284907ce5e66..25dc86cd674d8 100644 --- a/projects/demo/src/modules/components/multi-select/multi-select.component.ts +++ b/projects/demo/src/modules/components/multi-select/multi-select.component.ts @@ -1,5 +1,4 @@ import {Component, forwardRef} from '@angular/core'; -import {FormControl} from '@angular/forms'; import {changeDetection} from '@demo/emulate/change-detection'; import {TuiDocExample, tuiDocExcludeProperties} from '@taiga-ui/addon-doc'; import { @@ -11,11 +10,10 @@ import { TuiIdentityMatcher, TuiStringHandler, } from '@taiga-ui/cdk'; -import {TuiSizeL, TuiSizeS} from '@taiga-ui/core'; import {PolymorpheusContent} from '@tinkoff/ng-polymorpheus'; -import {AbstractExampleTuiControl} from '../abstract/control'; import {ABSTRACT_PROPS_ACCESSOR} from '../abstract/inherited-documentation/abstract-props-accessor'; +import {AbstractExampleTuiMulti} from '../abstract/multi'; class Account { constructor( @@ -46,7 +44,7 @@ class Account { ]), ], }) -export class ExampleTuiMultiSelectComponent extends AbstractExampleTuiControl { +export class ExampleTuiMultiSelectComponent extends AbstractExampleTuiMulti { readonly exampleModule = import('./examples/import/import-module.md?raw'); readonly exampleHtml = import('./examples/import/insert-template.md?raw'); readonly exampleForm = import('./examples/import/declare-form.md?raw'); @@ -119,16 +117,6 @@ export class ExampleTuiMultiSelectComponent extends AbstractExampleTuiControl { new Account('Yuan', 237), ]; - expandable = true; - - rows = 100; - - editable = true; - - search: string | null = ''; - - override readonly sizeVariants: ReadonlyArray = ['s', 'm', 'l']; - readonly iconVariants = [ '', 'tuiIconSearchLarge', @@ -138,8 +126,6 @@ export class ExampleTuiMultiSelectComponent extends AbstractExampleTuiControl { override iconLeft = ''; - override size: TuiSizeL | TuiSizeS = this.sizeVariants[this.sizeVariants.length - 1]; - stringifyVariants: Array> = [ TUI_DEFAULT_STRINGIFY, item => String(String(item).match(/\d+/)), @@ -172,8 +158,6 @@ export class ExampleTuiMultiSelectComponent extends AbstractExampleTuiControl { valueContent = this.valueContentVariants[0]; - control = new FormControl(); - readonly disabledItemHandlerVariants: ReadonlyArray> = [ ALWAYS_FALSE_HANDLER, (item: Account) => item.balance < 300, diff --git a/projects/kit/components/input-date/index.ts b/projects/kit/components/input-date/index.ts index bf1757ea6bd48..d667ec0fdf562 100644 --- a/projects/kit/components/input-date/index.ts +++ b/projects/kit/components/input-date/index.ts @@ -1,3 +1,6 @@ export * from './input-date.component'; export * from './input-date.directive'; export * from './input-date.module'; +export * from './multi-date/input-date-multi.component'; +export * from './multi-date/input-date-multi.directive'; +export * from './native-date/native-date.component'; diff --git a/projects/kit/components/input-date/input-date.component.ts b/projects/kit/components/input-date/input-date.component.ts index b7d4c42485396..70013e7808be1 100644 --- a/projects/kit/components/input-date/input-date.component.ts +++ b/projects/kit/components/input-date/input-date.component.ts @@ -63,7 +63,7 @@ import {Observable} from 'rxjs'; import {map, takeUntil} from 'rxjs/operators'; @Component({ - selector: 'tui-input-date', + selector: 'tui-input-date:not([multiple])', templateUrl: './input-date.template.html', styleUrls: ['./input-date.style.less'], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/projects/kit/components/input-date/input-date.directive.ts b/projects/kit/components/input-date/input-date.directive.ts index ad7b5b29154b7..f1137890b3e56 100644 --- a/projects/kit/components/input-date/input-date.directive.ts +++ b/projects/kit/components/input-date/input-date.directive.ts @@ -5,7 +5,7 @@ import {AbstractTuiTextfieldHost, tuiAsTextfieldHost} from '@taiga-ui/core'; import {TuiInputDateComponent} from './input-date.component'; @Directive({ - selector: 'tui-input-date', + selector: 'tui-input-date:not([multiple])', providers: [tuiAsTextfieldHost(TuiInputDateDirective)], }) export class TuiInputDateDirective extends AbstractTuiTextfieldHost { diff --git a/projects/kit/components/input-date/input-date.module.ts b/projects/kit/components/input-date/input-date.module.ts index efdf1a1accdb8..7efa50ea83263 100644 --- a/projects/kit/components/input-date/input-date.module.ts +++ b/projects/kit/components/input-date/input-date.module.ts @@ -1,7 +1,8 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; import {MaskitoModule} from '@maskito/angular'; -import {TuiLetModule, TuiPreventDefaultModule} from '@taiga-ui/cdk'; +import {TuiLetModule, TuiMapperPipeModule, TuiPreventDefaultModule} from '@taiga-ui/cdk'; import { TuiCalendarModule, TuiHostedDropdownModule, @@ -12,11 +13,14 @@ import { TuiTextfieldControllerModule, TuiWrapperModule, } from '@taiga-ui/core'; +import {TuiInputTagModule} from '@taiga-ui/kit/components/input-tag'; import {TuiValueAccessorModule} from '@taiga-ui/kit/directives'; import {PolymorpheusModule} from '@tinkoff/ng-polymorpheus'; import {TuiInputDateComponent} from './input-date.component'; import {TuiInputDateDirective} from './input-date.directive'; +import {TuiInputDateMultiComponent} from './multi-date/input-date-multi.component'; +import {TuiInputDateMultiDirective} from './multi-date/input-date-multi.directive'; import {TuiNativeDateDirective} from './native-date/native-date.component'; @NgModule({ @@ -34,8 +38,23 @@ import {TuiNativeDateDirective} from './native-date/native-date.component'; TuiValueAccessorModule, TuiLetModule, TuiTextfieldControllerModule, + TuiInputTagModule, + FormsModule, + TuiMapperPipeModule, + ], + declarations: [ + TuiInputDateComponent, + TuiInputDateDirective, + TuiInputDateMultiDirective, + TuiNativeDateDirective, + TuiInputDateMultiComponent, + ], + exports: [ + TuiInputDateComponent, + TuiInputDateDirective, + TuiInputDateMultiDirective, + TuiTextfieldComponent, + TuiInputDateMultiComponent, ], - declarations: [TuiInputDateComponent, TuiInputDateDirective, TuiNativeDateDirective], - exports: [TuiInputDateComponent, TuiInputDateDirective, TuiTextfieldComponent], }) export class TuiInputDateModule {} diff --git a/projects/kit/components/input-date/multi-date/input-date-multi.component.ts b/projects/kit/components/input-date/multi-date/input-date-multi.component.ts new file mode 100644 index 0000000000000..0ae83e56f0f73 --- /dev/null +++ b/projects/kit/components/input-date/multi-date/input-date-multi.component.ts @@ -0,0 +1,347 @@ +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 {TuiNamedDay, 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.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() + items: readonly TuiNamedDay[] = []; + + @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; + + @Input() + maskitoOptions: MaskitoOptions = maskitoDateOptionsGenerator({ + mode: 'dd/mm/yyyy', + separator: '.', + min: this.min?.toLocalNativeDate(), + max: this.max?.toLocalNativeDate(), + }); + + internalValue: readonly TuiDay[] = []; + + 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; + + return ( + (TuiDay.isValidDay(year, month, day) && + this.min && + TuiDay.normalizeOf(year, month, day).toLocalNativeDate() >= + this.min.toLocalNativeDate() && + this.max && + TuiDay.normalizeOf(year, month, day).toLocalNativeDate() <= + this.max.toLocalNativeDate()) ?? + 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 { + if (this.items[0] && this.value?.find(day => day.daySame(this.items[0].day))) { + return this.items[0].displayDay; + } + + return ( + this.month || + this.value[0] || + tuiDateClamp(this.defaultActiveYearMonth, this.computedMin, this.computedMax) + ); + } + + get nativeValue(): string { + return this.nativeFocusableElement?.value ?? ''; + } + + set nativeValue(value: string) { + if (!this.nativeFocusableElement) { + return; + } + + this.nativeFocusableElement.value = value.toString(); + } + + 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.internalValue = value; + this.value = value; + }); + } + + onEnter(search: string): void { + if (!this.tagValidator(search)) { + return; + } + + this.internalValue = tuiImmutableUpdateInputDateMulti( + this.internalValue, + 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.internalValue = value; + this.value = value; + } + + onDayClick(value: TuiDay): void { + this.internalValue = tuiImmutableUpdateInputDateMulti(this.internalValue, value); + } + + done(): void { + this.value = this.internalValue; + 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; + } + + override writeValue(value: readonly TuiDay[]): void { + this.internalValue = value; + super.writeValue(value); + } +} diff --git a/projects/kit/components/input-date/multi-date/input-date-multi.directive.ts b/projects/kit/components/input-date/multi-date/input-date-multi.directive.ts new file mode 100644 index 0000000000000..8a1185cc96f55 --- /dev/null +++ b/projects/kit/components/input-date/multi-date/input-date-multi.directive.ts @@ -0,0 +1,34 @@ +import {Directive} from '@angular/core'; +import {TuiDay} from '@taiga-ui/cdk'; +import {AbstractTuiTextfieldHost, tuiAsTextfieldHost} from '@taiga-ui/core'; +import {tuiImmutableUpdateInputDateMulti} from '@taiga-ui/kit/utils'; + +import {TuiInputDateMultiComponent} from './input-date-multi.component'; + +@Directive({ + selector: 'tui-input-date[multiple]', + providers: [tuiAsTextfieldHost(TuiInputDateMultiDirective)], +}) +export class TuiInputDateMultiDirective extends AbstractTuiTextfieldHost { + get max(): TuiDay { + return this.host.computedMax; + } + + get min(): TuiDay { + return this.host.computedMin; + } + + onValueChange(value: string): void { + // TODO: investigate i18n + const values = tuiImmutableUpdateInputDateMulti( + this.host.value, + TuiDay.normalizeParse(value), + ); + + this.host.onValueChange(values); + } + + override process(input: HTMLInputElement): void { + input.inputMode = 'numeric'; + } +} diff --git a/projects/kit/components/input-date/multi-date/input-date-multi.template.html b/projects/kit/components/input-date/multi-date/input-date-multi.template.html new file mode 100644 index 0000000000000..90ce0f91d65b1 --- /dev/null +++ b/projects/kit/components/input-date/multi-date/input-date-multi.template.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + +
+ +
+
+
diff --git a/projects/kit/components/input-tag/input-tag.component.ts b/projects/kit/components/input-tag/input-tag.component.ts index c716b7b7f56cb..02c4b870d094e 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 0000000000000..c37c258c7c13c --- /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 0000000000000..bebf62dcb5e51 --- /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 0000000000000..ddc5ef92b7607 --- /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 f51f1fe36d306..f5a94ead13570 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';