diff --git a/apps/ngx-bootstrap-docs-e2e/src/full/datepicker_page.spec.ts b/apps/ngx-bootstrap-docs-e2e/src/full/datepicker_page.spec.ts new file mode 100644 index 0000000000..c8475bd163 --- /dev/null +++ b/apps/ngx-bootstrap-docs-e2e/src/full/datepicker_page.spec.ts @@ -0,0 +1,65 @@ +import { test as base } from '@playwright/test'; +import { DatepickerPwPo } from '../support/datepicker.pw.po'; + +const test = base.extend<{ datepickerPo: DatepickerPwPo }>({ + datepickerPo: async ({ page }, use) => { + const datepickerPo = new DatepickerPwPo(page); + await use(datepickerPo); + }, +}); +test.describe('Datepicker page testing suite', () => { + let tabSelector: string; + + test.beforeEach(async ({ datepickerPo }) => { + tabSelector = datepickerPo.getTabSelector('Overview'); + await datepickerPo.navigateTo(); + }); + + test.describe('Unlinked calendar', () => { + let unlinkedCalendars: string; + + test.beforeEach(async ({ datepickerPo }) => { + unlinkedCalendars = tabSelector + datepickerPo.exampleDemosArr.unlinkedCalendars; + await datepickerPo.scrollToMenu('Unlinked calendars'); + }); + + test('click should open two calendars', async ({ datepickerPo }) => { + await datepickerPo.clickOnDaterangepickerInput(unlinkedCalendars, 0); + await datepickerPo.waitForElementToBeVisible('bs-daterangepicker-container'); + await datepickerPo.expectItemVisible('.bs-datepicker-body', 0); + await datepickerPo.expectItemVisible('.bs-datepicker-body', 1); + }); + + test('when user goes to previous in the left calendar, right one stays the same', async ({ datepickerPo }) => { + await datepickerPo.clickOnDaterangepickerInput(unlinkedCalendars, 0); + await datepickerPo.waitForElementToBeVisible('bs-daterangepicker-container'); + await datepickerPo.expectTextInViewInTheHeader(0, 0, 'October'); + await datepickerPo.expectTextInViewInTheHeader(0, 1, '1979'); + await datepickerPo.expectTextInViewInTheHeader(1, 0, 'April'); + await datepickerPo.expectTextInViewInTheHeader(1, 1, '1985'); + await datepickerPo.clickOnNavigation(0, '<'); + await datepickerPo.expectTextInViewInTheHeader(0, 0, 'September'); + await datepickerPo.expectTextInViewInTheHeader(0, 1, '1979'); + await datepickerPo.expectTextInViewInTheHeader(1, 0, 'April'); + await datepickerPo.expectTextInViewInTheHeader(1, 1, '1985'); + }); + + test('when user changes mode to month on the calendar, right one stays the same', async ({ datepickerPo }) => { + await datepickerPo.clickOnDaterangepickerInput(unlinkedCalendars, 0); + await datepickerPo.waitForElementToBeVisible('bs-daterangepicker-container'); + await datepickerPo.expectTextInViewInTheHeader(0, 0, 'October'); + await datepickerPo.expectTextInViewInTheHeader(0, 1, '1979'); + await datepickerPo.expectTextInViewInTheHeader(1, 0, 'April'); + await datepickerPo.expectTextInViewInTheHeader(1, 1, '1985'); + await datepickerPo.clickOnNavigation(0, 'month'); + await datepickerPo.expectTextInViewInTheHeader(0, 0, '1979'); + await datepickerPo.expectTextInViewInTheBody(0, 'October', true); + await datepickerPo.expectTextInViewInTheBody(0, 'September', true); + await datepickerPo.expectTextInViewInTheHeader(1, 0, 'April'); + await datepickerPo.expectTextInViewInTheHeader(1, 1, '1985'); + await datepickerPo.expectTextInViewInTheBody(1, 'October', false); + await datepickerPo.expectTextInViewInTheBody(1, 'September', false); + }); + }); + +}); diff --git a/apps/ngx-bootstrap-docs-e2e/src/support/datepicker.pw.po.ts b/apps/ngx-bootstrap-docs-e2e/src/support/datepicker.pw.po.ts new file mode 100644 index 0000000000..5880264010 --- /dev/null +++ b/apps/ngx-bootstrap-docs-e2e/src/support/datepicker.pw.po.ts @@ -0,0 +1,108 @@ +// Todo: remove eslint-disable +/* eslint-disable @typescript-eslint/ban-ts-comment,@typescript-eslint/no-explicit-any */ +import { BasePo } from './base.po'; +import { expect } from '@playwright/test'; + +export class DatepickerPwPo extends BasePo { + override pageUrl = '/ngx-bootstrap/components/datepicker'; + pageTitle = 'Datepicker'; + ghLinkToComponent = 'https://github.com/valor-software/ngx-bootstrap/tree/development/src/datepicker'; + + datepickerInput = 'input[bsdatepicker]'; + daterangepickerInput = 'input[bsdaterangepicker]'; + datepickerNavView = 'bs-datepicker-navigation-view'; + datepickerContainer = 'bs-datepicker-container'; + datepickerInlineContainer = 'bs-datepicker-inline-container'; + daterangepickerContainer = 'bs-daterangepicker-container'; + datepickerBodyDaysView = 'bs-days-calendar-view'; + datepickerBodyMonthView = 'bs-month-calendar-view'; + datepickerBodyYearsView = 'bs-years-calendar-view'; + daterangepickerQuickSelectContainer = 'bs-custom-date-view'; + monthNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + 'January' + ]; + locales = []; + + exampleDemosArr = { + unlinkedCalendars: ' datepicker-unlinked-calendar-views' + }; + + async clickOnDatepickerInput(baseSelector: string, datepickerIndex = 0) { + const datepicker = this.page.locator(`${baseSelector} ${this.datepickerInput}`).nth(datepickerIndex); + await datepicker.waitFor({state: 'visible', timeout: 10000}); + await datepicker.click(); + } + + async clickOnDaterangepickerInput(baseSelector: string, dateRangeIndex = 0) { + const datepicker = this.page.locator(`${baseSelector} ${this.daterangepickerInput}`).nth(dateRangeIndex); + await datepicker.waitFor({state: 'visible', timeout: 10000}); + await datepicker.click(); + } + async waitForElementToBeVisible(selector: string) { + await this.page.waitForSelector(selector, { state: 'visible', timeout: 10000 }); + } + async expectItemVisible(selector: string, index: number) { + await expect(await this.page.locator(selector).nth(index)).toBeVisible({visible: true, timeout: 10000}); + } + + async expectTextInViewInTheHeader(calendarIndex: number, currentIndex: number, text: string) { + // this is needed so it won't return up with the inline range pickers that are also on the page + const mainPopup = await this.page.locator('bs-daterangepicker-container'); + const header = await mainPopup.locator('.bs-datepicker-head').nth(calendarIndex); + await expect(header).toBeVisible({visible: true, timeout: 10000}); + const current = await header.locator('.current').nth(currentIndex); + await expect(current).toBeVisible({visible: true, timeout: 10000}); + const button = await current.locator('span'); + await expect(button).toBeVisible({visible: true, timeout: 10000}); + await expect(button).toHaveText(text); + } + + async clickOnNavigation(dateRangeIndex = 0, navigationItem: '<' | '>' | 'month' | 'year' ) { + const mainPopup = await this.page.locator('bs-daterangepicker-container'); + const datepicker = await mainPopup.locator(`.bs-datepicker-head`).nth(dateRangeIndex); + switch (navigationItem) { + case '<': + await datepicker.locator('.previous').click(); + break; + + case '>': + await datepicker.locator('.next').click(); + break; + + case 'month': + await datepicker.locator('.current').nth(0).click(); + break; + + case 'year': + await datepicker.locator('.current').nth(1).click(); + break; + + default: + throw new Error('Unknown navigation item, correct: <, >, month, year'); + } + await this.page.waitForTimeout(200); // waiting for the navigation to happen + } + async expectTextInViewInTheBody(calendarIndex: number, text: string, visible: boolean) { + // this is needed so it won't return up with the inline range pickers that are also on the page + const mainPopup = await this.page.locator('bs-daterangepicker-container'); + const body = await mainPopup.locator('.bs-datepicker-body').nth(calendarIndex); + if (visible) { + await expect(body.getByText(text).first()).toBeVisible(); + } else { + await expect(body.getByText(text)).toHaveCount(0); + } + } + +} diff --git a/apps/ngx-bootstrap-docs/src/ng-api-doc.ts b/apps/ngx-bootstrap-docs/src/ng-api-doc.ts index 507066b4ad..b5b65d2ca7 100644 --- a/apps/ngx-bootstrap-docs/src/ng-api-doc.ts +++ b/apps/ngx-bootstrap-docs/src/ng-api-doc.ts @@ -742,6 +742,12 @@ export const ngdoc: any = { defaultValue: 'false', type: 'boolean', description: '

Shows timepicker under datepicker

\n' + }, + { + name: 'unlinkedCalendars', + defaultValue: 'false', + type: 'boolean', + description: '

Allow date range picker to switch the calendars separately

\n' } ] }, diff --git a/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts b/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts index 8544ebeb82..791b3e0950 100644 --- a/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts +++ b/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts @@ -53,6 +53,7 @@ import { DemoDatepickerPreventChangeToNextMonthComponent } from './demos/prevent import { DemoDatepickerWithTimepickerComponent } from './demos/with-timepicker/with-timepicker'; import { DatepickerCloseBehaviorComponent } from './demos/closeBehaviour/datepicker-close-behavior'; import { KeepDatesOutOfRulesComponent } from './demos/keep-dates-out-of-rules/keep-dates-out-of-rules.component'; +import { UnlinkedCalendarsComponent } from './demos/unlinked-calendar-views/unlinked-calendar-views.component'; export const demoComponentContent: ContentSection[] = [ { @@ -478,6 +479,14 @@ export const demoComponentContent: ContentSection[] = [ html: require('!!raw-loader!./demos/keep-dates-out-of-rules/keep-dates-out-of-rules.component.html'), description: `

If you use datepicker with rules (minDate, maxDate) you can set config property keepDatesOutOfRules to true to avoid overwriting invalid dates. Default value is false.

`, outlet: KeepDatesOutOfRulesComponent + }, + { + title: "Unlinked calendars", + anchor: 'unlinked-calendar-views', + component: require('!!raw-loader!./demos/unlinked-calendar-views/unlinked-calendar-views.component'), + html: require('!!raw-loader!./demos/unlinked-calendar-views/unlinked-calendar-views.component.html'), + description: `

If you use daterangepicker and don't want the calendars to move together you can set config property unlinkedCalendars to true. Default value is false.

`, + outlet: UnlinkedCalendarsComponent } ] }, @@ -737,6 +746,11 @@ export const demoComponentContent: ContentSection[] = [ anchor: 'keep-dates-out-of-rules', outlet: KeepDatesOutOfRulesComponent }, + { + title: "Unlinked calendars", + anchor: 'unlinked-calendar-views', + outlet: UnlinkedCalendarsComponent + }, ] } ]; diff --git a/libs/doc-pages/datepicker/src/lib/demos/index.ts b/libs/doc-pages/datepicker/src/lib/demos/index.ts index f79d2fbabb..71b2759b0c 100644 --- a/libs/doc-pages/datepicker/src/lib/demos/index.ts +++ b/libs/doc-pages/datepicker/src/lib/demos/index.ts @@ -45,6 +45,7 @@ import { DemoDatepickerPreventChangeToNextMonthComponent } from './prevent-chang import { DemoDatepickerWithTimepickerComponent } from './with-timepicker/with-timepicker'; import { DatepickerCloseBehaviorComponent } from './closeBehaviour/datepicker-close-behavior'; import { KeepDatesOutOfRulesComponent } from './keep-dates-out-of-rules/keep-dates-out-of-rules.component'; +import { UnlinkedCalendarsComponent } from './unlinked-calendar-views/unlinked-calendar-views.component'; export const DEMO_COMPONENTS = [ DemoDatePickerAdaptivePositionComponent, @@ -90,5 +91,6 @@ export const DEMO_COMPONENTS = [ DemoDatepickerStartViewComponent, DemoDatepickerWithTimepickerComponent, DatepickerCloseBehaviorComponent, - KeepDatesOutOfRulesComponent + KeepDatesOutOfRulesComponent, + UnlinkedCalendarsComponent ]; diff --git a/libs/doc-pages/datepicker/src/lib/demos/unlinked-calendar-views/unlinked-calendar-views.component.html b/libs/doc-pages/datepicker/src/lib/demos/unlinked-calendar-views/unlinked-calendar-views.component.html new file mode 100644 index 0000000000..23898a0584 --- /dev/null +++ b/libs/doc-pages/datepicker/src/lib/demos/unlinked-calendar-views/unlinked-calendar-views.component.html @@ -0,0 +1,11 @@ +
+
+ +
+
diff --git a/libs/doc-pages/datepicker/src/lib/demos/unlinked-calendar-views/unlinked-calendar-views.component.ts b/libs/doc-pages/datepicker/src/lib/demos/unlinked-calendar-views/unlinked-calendar-views.component.ts new file mode 100644 index 0000000000..68645d6223 --- /dev/null +++ b/libs/doc-pages/datepicker/src/lib/demos/unlinked-calendar-views/unlinked-calendar-views.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'datepicker-unlinked-calendar-views', + templateUrl: './unlinked-calendar-views.component.html', + standalone: false +}) +export class UnlinkedCalendarsComponent { + dateRangePickerValue?: (Date | undefined)[]; + range1: Date = new Date(1979, 9, 27); + range2: Date = new Date(1985, 3, 2); + ngOnInit(): void { + this.dateRangePickerValue = [this.range1, this.range2]; + } +} diff --git a/src/datepicker/base/bs-datepicker-container.ts b/src/datepicker/base/bs-datepicker-container.ts index 0d8ce5073e..36d38f6c9b 100644 --- a/src/datepicker/base/bs-datepicker-container.ts +++ b/src/datepicker/base/bs-datepicker-container.ts @@ -14,7 +14,8 @@ import { DayViewModel, MonthsCalendarViewModel, WeekViewModel, - YearsCalendarViewModel + YearsCalendarViewModel, + ComplexCalendarViewModel } from '../models'; export abstract class BsDatepickerAbstractComponent { @@ -36,6 +37,7 @@ export abstract class BsDatepickerAbstractComponent { isRangePicker?: boolean; withTimepicker?: boolean; + unlinkedCalendars?: boolean; set minDate(value: Date|undefined) { this._effects?.setMinDate(value); @@ -75,6 +77,8 @@ export abstract class BsDatepickerAbstractComponent { _daysCalendar$!: Observable; _daysCalendarSub = new Subscription(); + complexCalendar?: ComplexCalendarViewModel[]|undefined; + set daysCalendar$(value: Observable) { this._daysCalendar$ = value; this._daysCalendarSub.unsubscribe(); @@ -92,10 +96,10 @@ export abstract class BsDatepickerAbstractComponent { // todo: valorkin fix // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function - setViewMode(event: BsDatepickerViewMode): void {} + setViewMode(event: BsDatepickerViewMode, source?: number): void {} // eslint-disable-next-line - navigateTo(event: BsNavigationEvent): void {} + navigateTo(event: BsNavigationEvent, source?: number): void {} // eslint-disable-next-line dayHoverHandler(event: CellHoverEvent): void {} @@ -110,16 +114,16 @@ export abstract class BsDatepickerAbstractComponent { yearHoverHandler(event: CellHoverEvent): void {} // eslint-disable-next-line - timeSelectHandler(date: Date, index: number): void {} + timeSelectHandler(date: Date, index: number, source?: number): void {} // eslint-disable-next-line - daySelectHandler(day: DayViewModel): void {} + daySelectHandler(day: DayViewModel, source?: number): void {} // eslint-disable-next-line - monthSelectHandler(event: CalendarCellViewModel): void {} + monthSelectHandler(event: CalendarCellViewModel, source?: number): void {} // eslint-disable-next-line - yearSelectHandler(event: CalendarCellViewModel): void {} + yearSelectHandler(event: CalendarCellViewModel, source?: number): void {} // eslint-disable-next-line setRangeOnCalendar(dates: BsCustomDates): void {} diff --git a/src/datepicker/bs-datepicker.config.ts b/src/datepicker/bs-datepicker.config.ts index ac735988ac..2501db7a72 100644 --- a/src/datepicker/bs-datepicker.config.ts +++ b/src/datepicker/bs-datepicker.config.ts @@ -195,4 +195,8 @@ export class BsDatepickerConfig implements DatepickerRenderOptions { * Allows keep invalid dates in range. Can be used with minDate, maxDate * */ keepDatesOutOfRules = false; + /** + * If true calendar views can be changed separately (dateRangePicker only) + */ + unlinkedCalendars = false; } diff --git a/src/datepicker/engine/flag-days-calendar.ts b/src/datepicker/engine/flag-days-calendar.ts index 061f41329f..f64c263117 100644 --- a/src/datepicker/engine/flag-days-calendar.ts +++ b/src/datepicker/engine/flag-days-calendar.ts @@ -31,6 +31,7 @@ export interface FlagDaysCalendarOptions { monthIndex: number; dateCustomClasses: DatepickerDateCustomClasses[]; dateTooltipTexts: DatepickerDateTooltipText[]; + unlinkedCalendars: boolean; } export function flagDaysCalendar( @@ -122,12 +123,14 @@ export function flagDaysCalendar( // todo: add check for linked calendars formattedMonth.hideLeftArrow = + !options.unlinkedCalendars && ( options.isDisabled || - (!!options.monthIndex && options.monthIndex > 0 && options.monthIndex !== options.displayMonths); + (!!options.monthIndex && options.monthIndex > 0 && options.monthIndex !== options.displayMonths)); formattedMonth.hideRightArrow = + !options.unlinkedCalendars && ( options.isDisabled || ((!!options.monthIndex || options.monthIndex === 0) && !!options.displayMonths && options.monthIndex < options.displayMonths && - options.monthIndex + 1 !== options.displayMonths); + options.monthIndex + 1 !== options.displayMonths)); formattedMonth.disableLeftArrow = isMonthDisabled( shiftDate(formattedMonth.month, { month: -1 }), diff --git a/src/datepicker/engine/flag-days.calendar.spec.ts b/src/datepicker/engine/flag-days.calendar.spec.ts index 637d7ceb06..97145594a3 100644 --- a/src/datepicker/engine/flag-days.calendar.spec.ts +++ b/src/datepicker/engine/flag-days.calendar.spec.ts @@ -5,6 +5,7 @@ describe('flag-days-calendar:', () => { it('should flag days as disabled when they are part of the datesDisabled', () => { const weekViewModel = { + unlinkedCalendars: false, month: new Date('2019-02-01'), weeks: [ { @@ -29,6 +30,7 @@ describe('flag-days-calendar:', () => { new Date('2019-02-09') ]; const result = flagDaysCalendar(weekViewModel, { + unlinkedCalendars: false, datesDisabled, isDisabled: false, minDate: new Date('2019-01-01'), @@ -51,6 +53,7 @@ describe('flag-days-calendar:', () => { it('should flag days as disabled when they are not part of the datesEnabled', () => { const weekViewModel = { + unlinkedCalendars: false, month: new Date('2020-02-01'), weeks: [ { @@ -75,6 +78,7 @@ describe('flag-days-calendar:', () => { new Date('2020-02-09') ]; const result = flagDaysCalendar(weekViewModel, { + unlinkedCalendars: false, datesEnabled, isDisabled: false, minDate: new Date('2020-01-01'), diff --git a/src/datepicker/engine/flag-months-calendar.ts b/src/datepicker/engine/flag-months-calendar.ts index 45f78ca97a..63c31fca73 100644 --- a/src/datepicker/engine/flag-months-calendar.ts +++ b/src/datepicker/engine/flag-months-calendar.ts @@ -16,6 +16,7 @@ export interface FlagMonthCalendarOptions { datesEnabled: Date[]; displayMonths: number; monthIndex: number; + unlinkedCalendars: boolean; } export function flagMonthsCalendar( @@ -59,10 +60,12 @@ export function flagMonthsCalendar( // todo: add check for linked calendars monthCalendar.hideLeftArrow = + !options.unlinkedCalendars && !!options.monthIndex && options.monthIndex > 0 && options.monthIndex !== options.displayMonths; monthCalendar.hideRightArrow = - (!!options.monthIndex || options.monthIndex === 0 ) + !options.unlinkedCalendars + && (!!options.monthIndex || options.monthIndex === 0 ) && (!!options.displayMonths || options.displayMonths === 0) && options.monthIndex < options.displayMonths && options.monthIndex + 1 !== options.displayMonths; diff --git a/src/datepicker/engine/flag-years-calendar.ts b/src/datepicker/engine/flag-years-calendar.ts index 6dd357fd12..99cdca51d1 100644 --- a/src/datepicker/engine/flag-years-calendar.ts +++ b/src/datepicker/engine/flag-years-calendar.ts @@ -13,6 +13,7 @@ export interface FlagYearsCalendarOptions { datesEnabled: Date[]; displayMonths: number; yearIndex: number; + unlinkedCalendars: boolean; } export function flagYearsCalendar( @@ -53,8 +54,10 @@ export function flagYearsCalendar( // todo: add check for linked calendars yearsCalendar.hideLeftArrow = + !options.unlinkedCalendars && !!options.yearIndex && options.yearIndex > 0 && options.yearIndex !== options.displayMonths; yearsCalendar.hideRightArrow = + !options.unlinkedCalendars && !!options.yearIndex && !!options.displayMonths && options.yearIndex < options.displayMonths && options.yearIndex + 1 !== options.displayMonths; diff --git a/src/datepicker/engine/format-days-calendar.ts b/src/datepicker/engine/format-days-calendar.ts index 43832b11e3..b611582531 100644 --- a/src/datepicker/engine/format-days-calendar.ts +++ b/src/datepicker/engine/format-days-calendar.ts @@ -38,7 +38,8 @@ export function formatDaysCalendar(daysCalendar: DaysCalendarModel, hideLeftArrow: false, hideRightArrow: false, disableLeftArrow: false, - disableRightArrow: false + disableRightArrow: false, + unlinkedCalendars: false, }; } diff --git a/src/datepicker/engine/format-months-calendar.ts b/src/datepicker/engine/format-months-calendar.ts index 0a260c9b6a..9a81d0bca9 100644 --- a/src/datepicker/engine/format-months-calendar.ts +++ b/src/datepicker/engine/format-months-calendar.ts @@ -34,6 +34,7 @@ export function formatMonthsCalendar( hideRightArrow: false, hideLeftArrow: false, disableRightArrow: false, - disableLeftArrow: false + disableLeftArrow: false, + unlinkedCalendars: false, }; } diff --git a/src/datepicker/engine/format-years-calendar.ts b/src/datepicker/engine/format-years-calendar.ts index 6a4b643dcb..c2b4d36613 100644 --- a/src/datepicker/engine/format-years-calendar.ts +++ b/src/datepicker/engine/format-years-calendar.ts @@ -34,7 +34,8 @@ export function formatYearsCalendar( hideLeftArrow: false, hideRightArrow: false, disableLeftArrow: false, - disableRightArrow: false + disableRightArrow: false, + unlinkedCalendars: false, }; } diff --git a/src/datepicker/models/index.ts b/src/datepicker/models/index.ts index f93b50091a..bbcce4cb6f 100644 --- a/src/datepicker/models/index.ts +++ b/src/datepicker/models/index.ts @@ -11,6 +11,7 @@ export interface NavigationViewModel { hideRightArrow: boolean; disableLeftArrow: boolean; disableRightArrow: boolean; + unlinkedCalendars: boolean; } export interface CalendarCellViewModel { @@ -52,6 +53,8 @@ export interface DaysCalendarViewModel extends NavigationViewModel { weekdays: string[]; } +export type ComplexCalendarViewModel = { mode: 'day', calendar: DaysCalendarViewModel } | { mode: 'month', calendar: MonthsCalendarViewModel} | { mode: 'year', calendar: YearsCalendarViewModel}; + /** *************** */ // months calendar export interface MonthsCalendarViewModel extends NavigationViewModel { diff --git a/src/datepicker/reducer/bs-datepicker.actions.ts b/src/datepicker/reducer/bs-datepicker.actions.ts index ee41b783da..1bf9ff09b8 100644 --- a/src/datepicker/reducer/bs-datepicker.actions.ts +++ b/src/datepicker/reducer/bs-datepicker.actions.ts @@ -7,7 +7,7 @@ import { CellHoverEvent, DatepickerRenderOptions, DatepickerDateCustomClasses, - DatepickerDateTooltipText + DatepickerDateTooltipText, } from '../models'; @Injectable({providedIn: 'platform'}) @@ -47,38 +47,38 @@ export class BsDatepickerActions { return { type: BsDatepickerActions.FLAG }; } - select(date?: Date): Action { + select(date?: Date, source?: number): Action { return { type: BsDatepickerActions.SELECT, - payload: date + payload: { date, source } }; } - selectTime(date: Date, index: number): Action { + selectTime(date: Date, index: number, source?: number): Action { return { type: BsDatepickerActions.SELECT_TIME, - payload: { date, index }, + payload: { date, index, source }, }; } - changeViewMode(event: BsDatepickerViewMode): Action { + changeViewMode(event: BsDatepickerViewMode, source?: number): Action { return { type: BsDatepickerActions.CHANGE_VIEWMODE, - payload: event + payload: { event, source } }; } - navigateTo(event: BsViewNavigationEvent): Action { + navigateTo(event: BsViewNavigationEvent, source?: number): Action { return { type: BsDatepickerActions.NAVIGATE_TO, - payload: event + payload: { event, source } }; } - navigateStep(step?: TimeUnit): Action { + navigateStep(step?: TimeUnit, source?: number): Action { return { type: BsDatepickerActions.NAVIGATE_OFFSET, - payload: step + payload: { step, source } }; } @@ -90,10 +90,10 @@ export class BsDatepickerActions { } // date range picker - selectRange(value?: (Date|undefined)[] | undefined): Action { + selectRange(value?: (Date|undefined)[] | undefined, source?: number): Action { return { type: BsDatepickerActions.SELECT_RANGE, - payload: value + payload: { value, source } }; } diff --git a/src/datepicker/reducer/bs-datepicker.effects.ts b/src/datepicker/reducer/bs-datepicker.effects.ts index b7c4ef61c2..23ac900b39 100644 --- a/src/datepicker/reducer/bs-datepicker.effects.ts +++ b/src/datepicker/reducer/bs-datepicker.effects.ts @@ -11,6 +11,7 @@ import { BsDatepickerViewMode, BsNavigationEvent, CellHoverEvent, + ComplexCalendarViewModel, DatepickerDateCustomClasses, DatepickerDateTooltipText, DatepickerRenderOptions, @@ -46,12 +47,12 @@ export class BsDatepickerEffects { /** setters */ - setValue(value?: Date): void { - this._store?.dispatch(this._actions.select(value)); + setValue(value?: Date, source?: number): void { + this._store?.dispatch(this._actions.select(value, source)); } - setRangeValue(value?: (Date|undefined)[] | undefined): void { - this._store?.dispatch(this._actions.selectRange(value)); + setRangeValue(value?: (Date|undefined)[] | undefined, source?: number): void { + this._store?.dispatch(this._actions.selectRange(value, source)); } setMinDate(value?: Date): BsDatepickerEffects { @@ -116,6 +117,7 @@ export class BsDatepickerEffects { return this; } + container.selectedTime = this._store.select(state => state.selectedTime) .pipe(filter(times => !!times)); @@ -142,17 +144,69 @@ export class BsDatepickerEffects { }) )); + this._subs.push( + combineLatest([ + this._store.select(state => state.viewStates?.map(state => state.mode)), + this._store.select(state => state.flaggedMonths), + this._store.select(state => state.flaggedMonthsCalendar), + this._store.select(state => state.yearsCalendarFlagged), + ]).pipe(map(([modes, days, months, years]) => { + if (modes == null) { + return undefined; + } + const calendars: ComplexCalendarViewModel[] = []; + for(let idx = 0; idx < modes.length; idx++) { + const flaggedMonth = days != null && days.length > idx ? days[idx] : undefined; + const flaggedMonthsCalendar = months != null && months.length > idx ? months[idx] : undefined; + const yearsCalendarFlagged = years != null && years.length > idx ? years[idx] : undefined; + let complex: ComplexCalendarViewModel | undefined; + switch (modes[idx]) { + case 'day': + complex = flaggedMonth != null ? { mode: 'day', calendar: flaggedMonth } : undefined; + break; + case 'month': + complex = flaggedMonthsCalendar != null ? { mode: 'month', calendar: flaggedMonthsCalendar } : undefined; + break; + case 'year': + complex = yearsCalendarFlagged != null ? { mode: 'year', calendar: yearsCalendarFlagged } : undefined; + break; + } + if (complex) { + calendars[idx] = complex; + } + } + return calendars; + }) + ).subscribe(complex => { + if(complex == null){ + return; + } + if ((container.complexCalendar?? []).length != complex.length) { + container.complexCalendar = new Array(complex.length); + } + complex.forEach((vm, idx) => { + if (container.complexCalendar![idx] == null) { + container.complexCalendar![idx] = vm; + } else { + const act = container.complexCalendar![idx]; + act.mode = vm.mode; + act.calendar = vm.calendar; + } + }) + }) + ); return this; } + /** event handlers */ setEventHandlers(container: BsDatepickerAbstractComponent): BsDatepickerEffects { - container.setViewMode = (event: BsDatepickerViewMode): void => { - this._store?.dispatch(this._actions.changeViewMode(event)); + container.setViewMode = (event: BsDatepickerViewMode, source?: number): void => { + this._store?.dispatch(this._actions.changeViewMode(event, source)); }; - container.navigateTo = (event: BsNavigationEvent): void => { - this._store?.dispatch(this._actions.navigateStep(event.step)); + container.navigateTo = (event: BsNavigationEvent, source: number): void => { + this._store?.dispatch(this._actions.navigateStep(event.step, source)); }; container.dayHoverHandler = (event: CellHoverEvent): void => { diff --git a/src/datepicker/reducer/bs-datepicker.reducer.ts b/src/datepicker/reducer/bs-datepicker.reducer.ts index 95f14ff29b..5e3ea7bde9 100644 --- a/src/datepicker/reducer/bs-datepicker.reducer.ts +++ b/src/datepicker/reducer/bs-datepicker.reducer.ts @@ -20,7 +20,7 @@ import { formatMonthsCalendar } from '../engine/format-months-calendar'; import { flagMonthsCalendar } from '../engine/flag-months-calendar'; import { formatYearsCalendar, initialYearShift, yearsPerCalendar } from '../engine/format-years-calendar'; import { flagYearsCalendar } from '../engine/flag-years-calendar'; -import { BsViewNavigationEvent, DatepickerFormatOptions, BsDatepickerViewMode } from '../models'; +import { BsViewNavigationEvent, DatepickerFormatOptions, BsDatepickerViewMode, DaysCalendarViewModel, MonthsCalendarViewModel, YearsCalendarViewModel } from '../models'; import { getYearsCalendarInitialDate } from '../utils/bs-calendar-utils'; import { copyTime } from '../utils/copy-time-utils'; @@ -45,7 +45,7 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker } case BsDatepickerActions.NAVIGATE_TO: { - const payload: BsViewNavigationEvent = action.payload; + const payload: BsViewNavigationEvent = action.payload.event; if (!state.view || !payload.unit) { return state; } @@ -55,24 +55,22 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker let mode: BsDatepickerViewMode; if (canSwitchMode(payload.viewMode, state.minMode)) { mode = payload.viewMode; - newState = { view: { date, mode } }; + newState = { view: { date, mode, source: action.payload.source } }; } else { mode = state.view.mode; - newState = { selectedDate: date, view: { date, mode } }; + newState = { selectedDate: date, view: { date, mode, source: action.payload.source } }; } - return Object.assign({}, state, newState); } case BsDatepickerActions.CHANGE_VIEWMODE: { - if (!canSwitchMode(action.payload, state.minMode) || !state.view) { + if (!canSwitchMode(action.payload.event, state.minMode) || !state.view) { return state; } const date = state.view.date; - const mode = action.payload; - const newState = { view: { date, mode } }; - + const mode = action.payload.event; + const newState = { view: { date, mode, source: action.payload.source } }; return Object.assign({}, state, newState); } @@ -84,10 +82,11 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker if (!state.view) { return state; } - + const source = action.payload.source; const newState = { - selectedDate: action.payload, - view: state.view + selectedDate: action.payload.date, + view: state.view, + source: source, }; if (Array.isArray(state.selectedTime)) { @@ -97,8 +96,8 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker } } - const mode = state.view.mode; - const _date = action.payload || state.view.date; + const mode = state.viewStates != null && source != null ? state.viewStates[source].mode : state.view.mode; + const _date = action.payload.date || state.view.date; const date = getViewDate(_date, state.minDate, state.maxDate); newState.view = { mode, date }; @@ -106,10 +105,10 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker } case BsDatepickerActions.SELECT_TIME: { - const {date, index} = action.payload; + const {date, index, source} = action.payload; const selectedTime = state.selectedTime ? [...state.selectedTime] : []; selectedTime[index] = date; - return Object.assign({}, state, { selectedTime }); + return Object.assign({}, state, { selectedTime, source }); } case BsDatepickerActions.SET_OPTIONS: { @@ -119,7 +118,7 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker const newState = action.payload; // preserve view mode - const mode = newState.minMode ? newState.minMode : state.view.mode; + const mode = newState.minMode ? newState.minMode : newState.startView; const _viewDate = isDateValid(newState.value) && newState.value || isArray(newState.value) && isDateValid(newState.value[0]) && newState.value[0] || state.view.date; @@ -153,8 +152,8 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker } const newState = { - selectedRange: action.payload, - view: state.view + selectedRange: action.payload.value, + view: state.view, }; newState.selectedRange?.forEach((dte: Date, index: number) => { if (Array.isArray(state.selectedTime)) { @@ -164,11 +163,11 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker } } }); - - const mode = state.view.mode; - const _date = action.payload && action.payload[0] || state.view.date; + const source = action.payload.source; + const mode = state.viewStates != null && source != null ? state.viewStates[source].mode : state.view.mode; + const _date = action.payload.value && action.payload.value[0] || state.view.date; const date = getViewDate(_date, state.minDate, state.maxDate); - newState.view = { mode, date }; + newState.view = { mode, date, source }; return Object.assign({}, state, newState); } @@ -208,7 +207,7 @@ function calculateReducer(state: BsDatepickerState): BsDatepickerState { if (!state.view) { return state; } - + const source = state.view.source; // how many calendars let displayMonths: number | undefined; if (state.displayOneMonthRange && @@ -221,110 +220,140 @@ function calculateReducer(state: BsDatepickerState): BsDatepickerState { // use selected date on initial rendering if set let viewDate = state.view.date; - if (state.view.mode === 'day' && state.monthViewOptions) { - if (state.showPreviousMonth && state.selectedRange && state.selectedRange.length === 0) { - viewDate = shiftDate(viewDate, { month: -1 }); - } - - state.monthViewOptions.firstDayOfWeek = getLocale(state.locale).firstDayOfWeek(); - let monthsModel = new Array(displayMonths); - for (let monthIndex = 0; monthIndex < displayMonths; monthIndex++) { - // todo: for unlinked calendars it will be harder - monthsModel[monthIndex] = calcDaysCalendar( - viewDate, - state.monthViewOptions - ); - viewDate = shiftDate(viewDate, { month: 1 }); + if (state.viewStates == null) { + state.viewStates = new Array(displayMonths); + } + let monthsModel = new Array(displayMonths); + const monthsCalendar = new Array(displayMonths); + const yearsCalendarModel = new Array(displayMonths); + for (let calendarIndex = 0; calendarIndex < displayMonths; calendarIndex++) { + if (source != null && state.viewStates?.length > calendarIndex && state.viewStates[calendarIndex] != null) { + if(state.unlinkedCalendars) { + if (source == calendarIndex) { + state.viewStates[calendarIndex].mode = state.view.mode; + } + } else { + state.viewStates[calendarIndex].mode = state.view.mode; + } } - // Check if parameter enabled and check if it's not months navigation event - if (state.preventChangeToNextMonth && state.flaggedMonths && state.hoveredDate) { - const viewMonth = calcDaysCalendar(state.view.date, state.monthViewOptions); - // Check if viewed right month same as in flaggedMonths state, then override months model with flaggedMonths - if (state.flaggedMonths.length && state.flaggedMonths[1].month.getMonth() === viewMonth.month.getMonth()) { - monthsModel = state.flaggedMonths - .map(item => { - if (state.monthViewOptions) { - return calcDaysCalendar( - item.month, - state.monthViewOptions - ); - } - return null; - }) - .filter(item => item !== null); + const checkedMode = state.viewStates?.length > calendarIndex ? state.viewStates[calendarIndex]?.mode ?? state.view.mode : state.view.mode; + if (checkedMode === 'day' && state.monthViewOptions != null) { + if (calendarIndex == 0) { + if (state.showPreviousMonth && state.selectedRange && state.selectedRange.length === 0) { + viewDate = shiftDate(viewDate, { month: -1 }); + } + state.monthViewOptions.firstDayOfWeek = getLocale(state.locale).firstDayOfWeek(); + } + if (source != null && state.unlinkedCalendars) { + viewDate = state.viewStates[calendarIndex].date; + if (calendarIndex == source) { + viewDate = shiftDate(viewDate, { month: state.view.direction }); + state.viewStates[calendarIndex] = { date: viewDate, mode: 'day' }; + } + monthsModel[calendarIndex] = calcDaysCalendar( + viewDate, + state.monthViewOptions + ); + } else { + if(calendarIndex == displayMonths -1 && state.unlinkedCalendars && (state.selectedRange ?? []).length == 2) { + viewDate = state.selectedRange![1]; + } + monthsModel[calendarIndex] = calcDaysCalendar( + viewDate, + state.monthViewOptions + ); + state.viewStates[calendarIndex] = { date: viewDate, mode: 'day' }; + viewDate = shiftDate(viewDate, { month: 1 }); + } + // Check if parameter enabled and check if it's not months navigation event + if ((calendarIndex == displayMonths -1) && !state.unlinkedCalendars && state.preventChangeToNextMonth && state.flaggedMonths && state.hoveredDate) { + const viewMonth = calcDaysCalendar(state.view.date, state.monthViewOptions); + // Check if viewed right month same as in flaggedMonths state, then override months model with flaggedMonths + if (state.flaggedMonths.length && state.flaggedMonths[1].month.getMonth() === viewMonth.month.getMonth()) { + monthsModel = state.flaggedMonths + .map(item => { + if (state.monthViewOptions) { + return calcDaysCalendar( + item.month, + state.monthViewOptions + ); + } + return null; + }) + .filter(item => item !== null); + } } } - - return Object.assign({}, state, { monthsModel }); - } - - if (state.view.mode === 'month') { - const monthsCalendar = new Array(displayMonths); - for ( - let calendarIndex = 0; - calendarIndex < displayMonths; - calendarIndex++ - ) { - // todo: for unlinked calendars it will be harder - monthsCalendar[calendarIndex] = formatMonthsCalendar( - viewDate, - getFormatOptions(state) - ); - viewDate = shiftDate(viewDate, { year: 1 }); + if (checkedMode === 'month') { + if (source != null && state.unlinkedCalendars) { + viewDate = state.viewStates[calendarIndex].date; + if (calendarIndex == source) { + viewDate = shiftDate(viewDate, { year: state.view.direction }); + state.viewStates[calendarIndex] = { date: viewDate, mode: 'month' }; + } + monthsCalendar[calendarIndex] = formatMonthsCalendar( + viewDate, + getFormatOptions(state) + ); + } else { + monthsCalendar[calendarIndex] = formatMonthsCalendar( + viewDate, + getFormatOptions(state) + ); + state.viewStates[calendarIndex] = { date: viewDate, mode: 'month' }; + viewDate = shiftDate(viewDate, { year: 1 }); + } } - - return Object.assign({}, state, { monthsCalendar }); - } - - if (state.view.mode === 'year') { - const yearsCalendarModel = new Array(displayMonths); - - for ( - let calendarIndex = 0; - calendarIndex < displayMonths; - calendarIndex++ - ) { - // todo: for unlinked calendars it will be harder - yearsCalendarModel[calendarIndex] = formatYearsCalendar( - viewDate, - getFormatOptions(state), - state.minMode === 'year' ? getYearsCalendarInitialDate(state, calendarIndex) : undefined - ); - viewDate = shiftDate(viewDate, { year: yearsPerCalendar }); + if (checkedMode === 'year') { + if (source != null && state.unlinkedCalendars) { + viewDate = state.viewStates[calendarIndex].date; + if (calendarIndex == source) { + viewDate = shiftDate(viewDate, { year: state.view.direction }); + state.viewStates[calendarIndex] = { date: viewDate, mode: 'year' }; + } + yearsCalendarModel[calendarIndex] = formatYearsCalendar( + viewDate, + getFormatOptions(state), + state.minMode === 'year' ? getYearsCalendarInitialDate(state, calendarIndex) : undefined + ); + } else { + yearsCalendarModel[calendarIndex] = formatYearsCalendar( + viewDate, + getFormatOptions(state), + state.minMode === 'year' ? getYearsCalendarInitialDate(state, calendarIndex) : undefined + ); + state.viewStates[calendarIndex] = { date: viewDate, mode: 'year' }; + viewDate = shiftDate(viewDate, { year: yearsPerCalendar }); + } } - - return Object.assign({}, state, { yearsCalendarModel }); } - - return state; + return Object.assign({}, state, { monthsModel, monthsCalendar, yearsCalendarModel }); } function formatReducer(state: BsDatepickerState): BsDatepickerState { if (!state.view) { return state; } - - if (state.view.mode === 'day' && state.monthsModel) { - const formattedMonths = state.monthsModel.map((month, monthIndex) => - formatDaysCalendar(month, getFormatOptions(state), monthIndex) - ); - - return Object.assign({}, state, { formattedMonths }); - } - - // how many calendars const displayMonths = state.displayMonths || 1; - // check initial rendering - // use selected date on initial rendering if set - let viewDate = state.view.date; - if (state.view.mode === 'month') { - const monthsCalendar = new Array(displayMonths); - for ( - let calendarIndex = 0; - calendarIndex < displayMonths; - calendarIndex++ - ) { + const formattedMonths: DaysCalendarViewModel[] = new Array(displayMonths); + const monthsCalendar: MonthsCalendarViewModel[] = new Array(displayMonths); + const yearsCalendarModel: YearsCalendarViewModel[] = new Array(displayMonths); + for ( + let calendarIndex = 0; + calendarIndex < displayMonths; + calendarIndex++ + ) { + const viewState = (state.viewStates != null && state.viewStates?.length > calendarIndex) ? state.viewStates[calendarIndex] : state.view; + const checkedMode = viewState.mode; + if (checkedMode === 'day' && state.monthsModel) { + formattedMonths[calendarIndex] = formatDaysCalendar(state.monthsModel[calendarIndex], getFormatOptions(state), calendarIndex) + } + // how many calendars + // check initial rendering + // use selected date on initial rendering if set + let viewDate = viewState.date; + if (checkedMode === 'month') { // todo: for unlinked calendars it will be harder monthsCalendar[calendarIndex] = formatMonthsCalendar( viewDate, @@ -333,39 +362,38 @@ function formatReducer(state: BsDatepickerState): BsDatepickerState { viewDate = shiftDate(viewDate, { year: 1 }); } - return Object.assign({}, state, { monthsCalendar }); - } - - if (state.view.mode === 'year') { - const yearsCalendarModel = new Array(displayMonths); - for ( - let calendarIndex = 0; - calendarIndex < displayMonths; - calendarIndex++ - ) { - // todo: for unlinked calendars it will be harder + if (checkedMode === 'year') { yearsCalendarModel[calendarIndex] = formatYearsCalendar( viewDate, getFormatOptions(state) ); viewDate = shiftDate(viewDate, { year: 16 }); } - - return Object.assign({}, state, { yearsCalendarModel }); } - return state; + const res = Object.assign({}, state, { + formattedMonths: formattedMonths, + monthsCalendar: monthsCalendar, + yearsCalendarModel: yearsCalendarModel + }); + return res; } function flagReducer(state: BsDatepickerState): BsDatepickerState { if (!state.view) { return state; } - const displayMonths = isDisplayOneMonth(state.view.date, state.minDate, state.maxDate) ? 1 : state.displayMonths; - if (state.formattedMonths && state.view.mode === 'day') { - const flaggedMonths = state.formattedMonths.map( - (formattedMonth, monthIndex) => + const flaggedMonths: DaysCalendarViewModel[] = new Array(displayMonths); + const flaggedMonthsCalendar: MonthsCalendarViewModel[] = new Array(displayMonths); + const yearsCalendarFlagged: YearsCalendarViewModel[] = new Array(displayMonths); + + for(let idx = 0; idx < displayMonths; idx++) { + const viewState = (state.viewStates != null && state.viewStates?.length > idx) ? state.viewStates[idx] : state.view; + const checkedState = viewState.mode; + if (state.formattedMonths && checkedState === 'day') { + const formattedMonth = state.formattedMonths[idx]; + flaggedMonths[idx] = flagDaysCalendar(formattedMonth, { isDisabled: state.isDisabled, minDate: state.minDate, @@ -379,17 +407,13 @@ function flagReducer(state: BsDatepickerState): BsDatepickerState { displayMonths, dateCustomClasses: state.dateCustomClasses, dateTooltipTexts: state.dateTooltipTexts, - monthIndex - }) - ); - - return Object.assign({}, state, { flaggedMonths }); - } - - if (state.view.mode === 'month' && state.monthsCalendar) { - const flaggedMonthsCalendar = state.monthsCalendar.map( - (formattedMonth, monthIndex) => - flagMonthsCalendar(formattedMonth, { + monthIndex: idx, + unlinkedCalendars: state.unlinkedCalendars, + }); + } + if (checkedState === 'month' && state.monthsCalendar) { + const formattedMonth = state.monthsCalendar[idx]; + flaggedMonthsCalendar[idx] = flagMonthsCalendar(formattedMonth, { isDisabled: state.isDisabled, minDate: state.minDate, maxDate: state.maxDate, @@ -399,51 +423,49 @@ function flagReducer(state: BsDatepickerState): BsDatepickerState { datesEnabled: state.datesEnabled, selectedRange: state.selectedRange, displayMonths, - monthIndex - }) - ); - - return Object.assign({}, state, { flaggedMonthsCalendar }); - } - - if (state.view.mode === 'year' && state.yearsCalendarModel) { - const yearsCalendarFlagged = state.yearsCalendarModel.map( - (formattedMonth, yearIndex) => - flagYearsCalendar(formattedMonth, { - isDisabled: state.isDisabled, - minDate: state.minDate, - maxDate: state.maxDate, - hoveredYear: state.hoveredYear, - selectedDate: state.selectedDate, - datesDisabled: state.datesDisabled, - datesEnabled: state.datesEnabled, - selectedRange: state.selectedRange, - displayMonths, - yearIndex - }) - ); - - return Object.assign({}, state, { yearsCalendarFlagged }); + monthIndex: idx, + unlinkedCalendars: state.unlinkedCalendars, + }); + } + if (checkedState === 'year' && state.yearsCalendarModel) { + const formattedMonth = state.yearsCalendarModel[idx]; + yearsCalendarFlagged[idx] = flagYearsCalendar(formattedMonth, { + isDisabled: state.isDisabled, + minDate: state.minDate, + maxDate: state.maxDate, + hoveredYear: state.hoveredYear, + selectedDate: state.selectedDate, + datesDisabled: state.datesDisabled, + datesEnabled: state.datesEnabled, + selectedRange: state.selectedRange, + displayMonths, + yearIndex: idx, + unlinkedCalendars: state.unlinkedCalendars, + }); + } } - return state; + return Object.assign({}, state, { flaggedMonths, flaggedMonthsCalendar, yearsCalendarFlagged }); } function navigateOffsetReducer(state: BsDatepickerState, action: Action): BsDatepickerState { + if (!state.view) { return state; } - - const date = shiftViewDate(state, action); + const date = shiftViewDate(state, { ...action, payload: action.payload.step }); if (!date) { return state; } - + const source = action.payload.source; const newState: {view: BsDatepickerViewState} = { view: { - mode: state.view.mode, - date - } + mode: source != null && state.viewStates != null ? state.viewStates[source].mode : state.view.mode, + date, + source, + direction: action.payload.step['month'] ?? action.payload.step['year'], + }, + }; return Object.assign({}, state, newState); diff --git a/src/datepicker/reducer/bs-datepicker.state.ts b/src/datepicker/reducer/bs-datepicker.state.ts index ee4c6490c1..85d90f7f30 100644 --- a/src/datepicker/reducer/bs-datepicker.state.ts +++ b/src/datepicker/reducer/bs-datepicker.state.ts @@ -8,7 +8,7 @@ import { DaysCalendarViewModel, MonthsCalendarViewModel, MonthViewOptions, - YearsCalendarViewModel + YearsCalendarViewModel, } from '../models'; import { defaultMonthOptions } from './_defaults'; import { BsDatepickerConfig } from '../bs-datepicker.config'; @@ -16,6 +16,8 @@ import { BsDatepickerConfig } from '../bs-datepicker.config'; export interface BsDatepickerViewState { date: Date; mode: BsDatepickerViewMode; + source?: number; + direction?: number; } export class BsDatepickerState @@ -29,6 +31,7 @@ export class BsDatepickerState // initial date of calendar, today by default view?: BsDatepickerViewState; + viewStates?: BsDatepickerViewState[]; isDisabled?: boolean; // bounds @@ -80,6 +83,8 @@ export class BsDatepickerState yearLabel?: string; weekNumbers?: string; + unlinkedCalendars?: boolean; + } const _initialView: BsDatepickerViewState = { date: new Date(), mode: 'day' }; diff --git a/src/datepicker/testing/bs-datepicker.reducer.spec.ts b/src/datepicker/testing/bs-datepicker.reducer.spec.ts index 1ba66f5c28..933dccc9b8 100644 --- a/src/datepicker/testing/bs-datepicker.reducer.spec.ts +++ b/src/datepicker/testing/bs-datepicker.reducer.spec.ts @@ -9,7 +9,7 @@ describe('BsDatepickerReducer.', () => { const state = initialDatepickerState; const action: Action = { type: BsDatepickerActions.NAVIGATE_TO, - payload: { unit: { year: 2017, month: 11 }, viewMode: 'month'} + payload: { event: { unit: { year: 2017, month: 11 }, viewMode: 'month'}} }; const reducer = bsDatepickerReducer(state, action); expect(reducer.view.mode).toEqual('month'); @@ -24,7 +24,7 @@ describe('BsDatepickerReducer.', () => { const action: Action = { type: BsDatepickerActions.NAVIGATE_TO, - payload: { unit: { year: 2017, month: 11, day: 1}, viewMode: 'day' } + payload: { event: { unit: { year: 2017, month: 11, day: 1}, viewMode: 'day' }} }; const reducer = bsDatepickerReducer(state, action); diff --git a/src/datepicker/testing/flaggedMonthsMock.ts b/src/datepicker/testing/flaggedMonthsMock.ts index 62a022ba9e..07878375a3 100644 --- a/src/datepicker/testing/flaggedMonthsMock.ts +++ b/src/datepicker/testing/flaggedMonthsMock.ts @@ -6,6 +6,7 @@ export const mockFlaggedMonths: DaysCalendarViewModel[] = [{ yearTitle: '2021', weekNumbers: ['10', '11', '12', '13', '14', '15'], weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + unlinkedCalendars: false, weeks: [{ days: [{ date: new Date('2021-02-28T13:36:16'), @@ -701,6 +702,7 @@ export const mockFlaggedMonths: DaysCalendarViewModel[] = [{ yearTitle: '2021', weekNumbers: ['14', '15', '16', '17', '18', '19'], weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + unlinkedCalendars: false, weeks: [{ days: [{ date: new Date('2021-03-28T12:36:16'), diff --git a/src/datepicker/themes/bs/bs-datepicker-container.component.ts b/src/datepicker/themes/bs/bs-datepicker-container.component.ts index 4d2d4b990a..2ab842929d 100644 --- a/src/datepicker/themes/bs/bs-datepicker-container.component.ts +++ b/src/datepicker/themes/bs/bs-datepicker-container.component.ts @@ -29,7 +29,7 @@ import { BsYearsCalendarViewComponent } from './bs-years-calendar-view.component import { BsMonthCalendarViewComponent } from './bs-months-calendar-view.component'; import { TimepickerModule } from 'ngx-bootstrap/timepicker'; import { BsDaysCalendarViewComponent } from './bs-days-calendar-view.component'; -import { NgIf, NgClass, NgSwitch, NgSwitchCase, NgFor, AsyncPipe } from '@angular/common'; +import { NgIf, NgClass, NgFor, AsyncPipe } from '@angular/common'; @Component({ selector: 'bs-datepicker-container', @@ -43,7 +43,7 @@ import { NgIf, NgClass, NgSwitch, NgSwitchCase, NgFor, AsyncPipe } from '@angula }, animations: [datepickerAnimation], standalone: true, - imports: [NgIf, NgClass, NgSwitch, NgSwitchCase, NgFor, BsDaysCalendarViewComponent, TimepickerModule, + imports: [NgIf, NgClass, NgFor, BsDaysCalendarViewComponent, TimepickerModule, BsMonthCalendarViewComponent, BsYearsCalendarViewComponent, BsCustomDatesViewComponent, AsyncPipe ] }) @@ -182,11 +182,11 @@ export class BsDatepickerContainerComponent this._positionService.enable(); } - override timeSelectHandler(date: Date, index: number) { - this._store.dispatch(this._actions.selectTime(date, index)); + override timeSelectHandler(date: Date, index: number, source?: number) { + this._store.dispatch(this._actions.selectTime(date, index, source)); } - override daySelectHandler(day: DayViewModel): void { + override daySelectHandler(day: DayViewModel, source?: number): void { if (!day) { return; } @@ -197,10 +197,10 @@ export class BsDatepickerContainerComponent return; } - this._store.dispatch(this._actions.select(day.date)); + this._store.dispatch(this._actions.select(day.date, source)); } - override monthSelectHandler(day: CalendarCellViewModel): void { + override monthSelectHandler(day: CalendarCellViewModel, source?: number): void { if (!day || day.isDisabled) { return; } @@ -212,11 +212,11 @@ export class BsDatepickerContainerComponent year: getFullYear(day.date) }, viewMode: 'day' - }) + }, source) ); } - override yearSelectHandler(day: CalendarCellViewModel): void { + override yearSelectHandler(day: CalendarCellViewModel, source?: number): void { if (!day || day.isDisabled) { return; } @@ -227,7 +227,7 @@ export class BsDatepickerContainerComponent year: getFullYear(day.date) }, viewMode: 'month' - }) + }, source) ); } diff --git a/src/datepicker/themes/bs/bs-datepicker-inline-container.component.ts b/src/datepicker/themes/bs/bs-datepicker-inline-container.component.ts index 6bf6c6d6c0..811e53faf9 100644 --- a/src/datepicker/themes/bs/bs-datepicker-inline-container.component.ts +++ b/src/datepicker/themes/bs/bs-datepicker-inline-container.component.ts @@ -13,7 +13,7 @@ import { BsYearsCalendarViewComponent } from './bs-years-calendar-view.component import { BsMonthCalendarViewComponent } from './bs-months-calendar-view.component'; import { TimepickerModule } from 'ngx-bootstrap/timepicker'; import { BsDaysCalendarViewComponent } from './bs-days-calendar-view.component'; -import { NgIf, NgClass, NgSwitch, NgSwitchCase, NgFor, AsyncPipe } from '@angular/common'; +import { NgIf, NgClass, NgFor, AsyncPipe } from '@angular/common'; @Component({ selector: 'bs-datepicker-inline-container', @@ -24,7 +24,7 @@ import { NgIf, NgClass, NgSwitch, NgSwitchCase, NgFor, AsyncPipe } from '@angula }, animations: [datepickerAnimation], standalone: true, - imports: [NgIf, NgClass, NgSwitch, NgSwitchCase, NgFor, BsDaysCalendarViewComponent, TimepickerModule, BsMonthCalendarViewComponent, BsYearsCalendarViewComponent, BsCustomDatesViewComponent, AsyncPipe] + imports: [NgIf, NgClass, NgFor, BsDaysCalendarViewComponent, TimepickerModule, BsMonthCalendarViewComponent, BsYearsCalendarViewComponent, BsCustomDatesViewComponent, AsyncPipe] }) export class BsDatepickerInlineContainerComponent extends BsDatepickerContainerComponent implements OnInit, OnDestroy { diff --git a/src/datepicker/themes/bs/bs-datepicker-view.html b/src/datepicker/themes/bs/bs-datepicker-view.html index 4fbfea672a..0419909e2a 100644 --- a/src/datepicker/themes/bs/bs-datepicker-view.html +++ b/src/datepicker/themes/bs/bs-datepicker-view.html @@ -4,56 +4,51 @@ [@datepickerAnimation]="animationState" (@datepickerAnimation.done)="positionServiceEnable()"> -
- - -
+
+
+ +
+ (onSelect)="daySelectHandler($event, idx)">
-
- - + +
+ +
- - - -
- - -
- -
- - + +
+ + +
- +
+ + +
diff --git a/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts index 47b7651495..7124010286 100644 --- a/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts +++ b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts @@ -30,7 +30,7 @@ import { BsYearsCalendarViewComponent } from './bs-years-calendar-view.component import { BsMonthCalendarViewComponent } from './bs-months-calendar-view.component'; import { TimepickerModule } from 'ngx-bootstrap/timepicker'; import { BsDaysCalendarViewComponent } from './bs-days-calendar-view.component'; -import { NgIf, NgClass, NgSwitch, NgSwitchCase, NgFor, AsyncPipe } from '@angular/common'; +import { NgIf, NgClass, NgFor, AsyncPipe } from '@angular/common'; @Component({ selector: 'bs-daterangepicker-container', @@ -47,8 +47,6 @@ import { NgIf, NgClass, NgSwitch, NgSwitchCase, NgFor, AsyncPipe } from '@angula imports: [ NgIf, NgClass, - NgSwitch, - NgSwitchCase, NgFor, BsDaysCalendarViewComponent, TimepickerModule, @@ -135,6 +133,7 @@ export class BsDaterangepickerContainerComponent this.containerClass = this._config.containerClass; this.isOtherMonthsActive = this._config.selectFromOtherMonth; this.withTimepicker = this._config.withTimepicker; + this.unlinkedCalendars = this._config.unlinkedCalendars; this._effects ?.init(this._store) // intial state options @@ -204,11 +203,11 @@ export class BsDaterangepickerContainerComponent this._positionService.enable(); } - override timeSelectHandler(date: Date, index: number): void { - this._store.dispatch(this._actions.selectTime(date, index)); + override timeSelectHandler(date: Date, index: number, source?: number): void { + this._store.dispatch(this._actions.selectTime(date, index, source)); } - override daySelectHandler(day: DayViewModel): void { + override daySelectHandler(day: DayViewModel, source?: number): void { if (!day) { return; } @@ -217,14 +216,13 @@ export class BsDaterangepickerContainerComponent if (isDisabled) { return; } - this.rangesProcessing(day); + this.rangesProcessing(day, source); } - override monthSelectHandler(day: CalendarCellViewModel): void { + override monthSelectHandler(day: CalendarCellViewModel, source?: number): void { if (!day || day.isDisabled) { return; } - day.isSelected = true; if (this._config.minMode !== 'month') { @@ -238,19 +236,18 @@ export class BsDaterangepickerContainerComponent year: getFullYear(day.date) }, viewMode: 'day' - }) + }, source) ); return; } - this.rangesProcessing(day); + this.rangesProcessing(day, source); } - override yearSelectHandler(day: CalendarCellViewModel): void { + override yearSelectHandler(day: CalendarCellViewModel, source?: number): void { if (!day || day.isDisabled) { return; } - day.isSelected = true; if (this._config.minMode !== 'year') { @@ -263,15 +260,15 @@ export class BsDaterangepickerContainerComponent year: getFullYear(day.date) }, viewMode: 'month' - }) + }, source) ); return; } - this.rangesProcessing(day); + this.rangesProcessing(day, source); } - rangesProcessing(day: CalendarCellViewModel): void { + rangesProcessing(day: CalendarCellViewModel, source?: number): void { // if only one date is already selected // and user clicks on previous date // start selection from new date @@ -294,7 +291,7 @@ export class BsDaterangepickerContainerComponent } } - this._store.dispatch(this._actions.selectRange(this._rangeStack)); + this._store.dispatch(this._actions.selectRange(this._rangeStack, source)); if (this._rangeStack.length === 2) { this._rangeStack = []; diff --git a/src/datepicker/themes/bs/bs-daterangepicker-inline-container.component.ts b/src/datepicker/themes/bs/bs-daterangepicker-inline-container.component.ts index d0437253af..fa003665fc 100644 --- a/src/datepicker/themes/bs/bs-daterangepicker-inline-container.component.ts +++ b/src/datepicker/themes/bs/bs-daterangepicker-inline-container.component.ts @@ -13,7 +13,7 @@ import { BsYearsCalendarViewComponent } from './bs-years-calendar-view.component import { BsMonthCalendarViewComponent } from './bs-months-calendar-view.component'; import { TimepickerModule } from 'ngx-bootstrap/timepicker'; import { BsDaysCalendarViewComponent } from './bs-days-calendar-view.component'; -import { NgIf, NgClass, NgSwitch, NgSwitchCase, NgFor, AsyncPipe } from '@angular/common'; +import { NgIf, NgClass, NgFor, AsyncPipe } from '@angular/common'; @Component({ selector: 'bs-daterangepicker-inline-container', @@ -24,7 +24,7 @@ import { NgIf, NgClass, NgSwitch, NgSwitchCase, NgFor, AsyncPipe } from '@angula }, animations: [datepickerAnimation], standalone: true, - imports: [NgIf, NgClass, NgSwitch, NgSwitchCase, NgFor, BsDaysCalendarViewComponent, TimepickerModule, BsMonthCalendarViewComponent, BsYearsCalendarViewComponent, BsCustomDatesViewComponent, AsyncPipe] + imports: [NgIf, NgClass, NgFor, BsDaysCalendarViewComponent, TimepickerModule, BsMonthCalendarViewComponent, BsYearsCalendarViewComponent, BsCustomDatesViewComponent, AsyncPipe] }) export class BsDaterangepickerInlineContainerComponent extends BsDaterangepickerContainerComponent implements OnInit, OnDestroy {