From db2ed3625d6ba679ad7e45508734ebadee718341 Mon Sep 17 00:00:00 2001 From: Maxie42 Date: Fri, 26 Sep 2025 23:40:45 +0200 Subject: [PATCH 1/8] setup demos --- apps/ngx-bootstrap-docs/src/ng-api-doc.ts | 6 ++++++ .../datepicker/src/lib/datepicker-section.list.ts | 14 ++++++++++++++ libs/doc-pages/datepicker/src/lib/demos/index.ts | 4 +++- .../unlinked-calendar-views.component.html | 10 ++++++++++ .../unlinked-calendar-views.component.ts | 13 +++++++++++++ src/datepicker/bs-datepicker.config.ts | 4 ++++ 6 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 libs/doc-pages/datepicker/src/lib/demos/unlinked-calendar-views/unlinked-calendar-views.component.html create mode 100644 libs/doc-pages/datepicker/src/lib/demos/unlinked-calendar-views/unlinked-calendar-views.component.ts 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..98b35770f0 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: "Allow separately moving calendars in DateRangePicker", + 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: "Allow separately moving calendars in DateRangePicker", + 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..465cc5ac85 --- /dev/null +++ b/libs/doc-pages/datepicker/src/lib/demos/unlinked-calendar-views/unlinked-calendar-views.component.html @@ -0,0 +1,10 @@ +
+
+ +
+
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..525327ac79 --- /dev/null +++ b/libs/doc-pages/datepicker/src/lib/demos/unlinked-calendar-views/unlinked-calendar-views.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'unlinked-calendar-views', + templateUrl: './unlinked-calendar-views.component.html', + standalone: false +}) +export class UnlinkedCalendarsComponent { + + constructor() { + } +} 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; } From f6d9def0dbba826887f98c4cf22245072c24f1e1 Mon Sep 17 00:00:00 2001 From: Maxie42 Date: Fri, 26 Sep 2025 23:42:09 +0200 Subject: [PATCH 2/8] allow showing previous and next if unlinked --- .../unlinked-calendar-views.component.ts | 3 --- src/datepicker/engine/flag-days-calendar.ts | 7 +++++-- src/datepicker/engine/flag-months-calendar.ts | 5 ++++- src/datepicker/engine/flag-years-calendar.ts | 3 +++ src/datepicker/engine/format-days-calendar.ts | 3 ++- src/datepicker/engine/format-months-calendar.ts | 3 ++- src/datepicker/engine/format-years-calendar.ts | 3 ++- src/datepicker/models/index.ts | 1 + src/datepicker/reducer/bs-datepicker.reducer.ts | 10 ++++++---- src/datepicker/reducer/bs-datepicker.state.ts | 1 + 10 files changed, 26 insertions(+), 13 deletions(-) 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 index 525327ac79..5051dd84b0 100644 --- 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 @@ -7,7 +7,4 @@ import { Component } from '@angular/core'; standalone: false }) export class UnlinkedCalendarsComponent { - - constructor() { - } } 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-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..63724a890e 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 { diff --git a/src/datepicker/reducer/bs-datepicker.reducer.ts b/src/datepicker/reducer/bs-datepicker.reducer.ts index 95f14ff29b..f65db83a63 100644 --- a/src/datepicker/reducer/bs-datepicker.reducer.ts +++ b/src/datepicker/reducer/bs-datepicker.reducer.ts @@ -208,7 +208,6 @@ function calculateReducer(state: BsDatepickerState): BsDatepickerState { if (!state.view) { return state; } - // how many calendars let displayMonths: number | undefined; if (state.displayOneMonthRange && @@ -379,7 +378,8 @@ function flagReducer(state: BsDatepickerState): BsDatepickerState { displayMonths, dateCustomClasses: state.dateCustomClasses, dateTooltipTexts: state.dateTooltipTexts, - monthIndex + monthIndex, + unlinkedCalendars: state.unlinkedCalendars, }) ); @@ -399,7 +399,8 @@ function flagReducer(state: BsDatepickerState): BsDatepickerState { datesEnabled: state.datesEnabled, selectedRange: state.selectedRange, displayMonths, - monthIndex + monthIndex, + unlinkedCalendars: state.unlinkedCalendars, }) ); @@ -419,7 +420,8 @@ function flagReducer(state: BsDatepickerState): BsDatepickerState { datesEnabled: state.datesEnabled, selectedRange: state.selectedRange, displayMonths, - yearIndex + yearIndex, + unlinkedCalendars: state.unlinkedCalendars, }) ); diff --git a/src/datepicker/reducer/bs-datepicker.state.ts b/src/datepicker/reducer/bs-datepicker.state.ts index ee4c6490c1..ccd4e0ef03 100644 --- a/src/datepicker/reducer/bs-datepicker.state.ts +++ b/src/datepicker/reducer/bs-datepicker.state.ts @@ -80,6 +80,7 @@ export class BsDatepickerState yearLabel?: string; weekNumbers?: string; + unlinkedCalendars?: boolean; } const _initialView: BsDatepickerViewState = { date: new Date(), mode: 'day' }; From 4e693637293059f986d40a1f3c19dc5d5565561b Mon Sep 17 00:00:00 2001 From: Maxie42 Date: Sat, 27 Sep 2025 22:47:23 +0200 Subject: [PATCH 3/8] allow switching between month/years separately with each calendar --- .../base/bs-datepicker-container.ts | 3 +- .../reducer/bs-datepicker.actions.ts | 9 +- .../reducer/bs-datepicker.effects.ts | 5 +- .../reducer/bs-datepicker.reducer.ts | 102 ++++++++++++------ src/datepicker/reducer/bs-datepicker.state.ts | 6 +- .../themes/bs/bs-datepicker-view.html | 12 +-- .../bs-daterangepicker-container.component.ts | 1 + 7 files changed, 94 insertions(+), 44 deletions(-) diff --git a/src/datepicker/base/bs-datepicker-container.ts b/src/datepicker/base/bs-datepicker-container.ts index 0d8ce5073e..99c11f0af4 100644 --- a/src/datepicker/base/bs-datepicker-container.ts +++ b/src/datepicker/base/bs-datepicker-container.ts @@ -36,6 +36,7 @@ export abstract class BsDatepickerAbstractComponent { isRangePicker?: boolean; withTimepicker?: boolean; + unlinkedCalendars?: boolean; set minDate(value: Date|undefined) { this._effects?.setMinDate(value); @@ -95,7 +96,7 @@ export abstract class BsDatepickerAbstractComponent { setViewMode(event: BsDatepickerViewMode): void {} // eslint-disable-next-line - navigateTo(event: BsNavigationEvent): void {} + navigateTo(event: BsNavigationEvent, source: number): void {} // eslint-disable-next-line dayHoverHandler(event: CellHoverEvent): void {} diff --git a/src/datepicker/reducer/bs-datepicker.actions.ts b/src/datepicker/reducer/bs-datepicker.actions.ts index ee41b783da..d48ebf6182 100644 --- a/src/datepicker/reducer/bs-datepicker.actions.ts +++ b/src/datepicker/reducer/bs-datepicker.actions.ts @@ -7,7 +7,8 @@ import { CellHoverEvent, DatepickerRenderOptions, DatepickerDateCustomClasses, - DatepickerDateTooltipText + DatepickerDateTooltipText, + BsNavigationDirection } from '../models'; @Injectable({providedIn: 'platform'}) @@ -64,7 +65,7 @@ export class BsDatepickerActions { changeViewMode(event: BsDatepickerViewMode): Action { return { type: BsDatepickerActions.CHANGE_VIEWMODE, - payload: event + payload: event, }; } @@ -75,10 +76,10 @@ export class BsDatepickerActions { }; } - navigateStep(step?: TimeUnit): Action { + navigateStep(step?: TimeUnit, source?: number): Action { return { type: BsDatepickerActions.NAVIGATE_OFFSET, - payload: step + payload: { step, source } }; } diff --git a/src/datepicker/reducer/bs-datepicker.effects.ts b/src/datepicker/reducer/bs-datepicker.effects.ts index b7c4ef61c2..e1ac14d4da 100644 --- a/src/datepicker/reducer/bs-datepicker.effects.ts +++ b/src/datepicker/reducer/bs-datepicker.effects.ts @@ -145,14 +145,15 @@ export class BsDatepickerEffects { return this; } + /** event handlers */ setEventHandlers(container: BsDatepickerAbstractComponent): BsDatepickerEffects { container.setViewMode = (event: BsDatepickerViewMode): void => { this._store?.dispatch(this._actions.changeViewMode(event)); }; - 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 f65db83a63..da68962c3a 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, BsNavigationDirection } from '../models'; import { getYearsCalendarInitialDate } from '../utils/bs-calendar-utils'; import { copyTime } from '../utils/copy-time-utils'; @@ -72,7 +72,6 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker const date = state.view.date; const mode = action.payload; const newState = { view: { date, mode } }; - return Object.assign({}, state, newState); } @@ -87,7 +86,7 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker const newState = { selectedDate: action.payload, - view: state.view + view: state.view, }; if (Array.isArray(state.selectedTime)) { @@ -154,7 +153,7 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker const newState = { selectedRange: action.payload, - view: state.view + view: state.view, }; newState.selectedRange?.forEach((dte: Date, index: number) => { if (Array.isArray(state.selectedTime)) { @@ -208,6 +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 && @@ -220,23 +220,38 @@ function calculateReducer(state: BsDatepickerState): BsDatepickerState { // use selected date on initial rendering if set let viewDate = state.view.date; + if (state.viewStates == null) { + state.viewStates = new Array(displayMonths); + } if (state.view.mode === 'day' && state.monthViewOptions) { - if (state.showPreviousMonth && state.selectedRange && state.selectedRange.length === 0) { + if (!state.unlinkedCalendars && 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 (source != null && state.unlinkedCalendars) { + viewDate = state.viewStates[monthIndex].date; + if (monthIndex == source) { + viewDate = shiftDate(viewDate, { month: state.view.direction }); + state.viewStates[monthIndex] = { date: viewDate, mode: 'day' }; + } + monthsModel[monthIndex] = calcDaysCalendar( + viewDate, + state.monthViewOptions + ); + } else { + monthsModel[monthIndex] = calcDaysCalendar( + viewDate, + state.monthViewOptions + ); + state.viewStates[monthIndex] = { date: viewDate, mode: 'day' }; + viewDate = shiftDate(viewDate, { month: 1 }); + } } // Check if parameter enabled and check if it's not months navigation event - if (state.preventChangeToNextMonth && state.flaggedMonths && state.hoveredDate) { + if (!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()) { @@ -264,12 +279,24 @@ function calculateReducer(state: BsDatepickerState): BsDatepickerState { calendarIndex < displayMonths; calendarIndex++ ) { - // todo: for unlinked calendars it will be harder - monthsCalendar[calendarIndex] = formatMonthsCalendar( - viewDate, - getFormatOptions(state) - ); - viewDate = shiftDate(viewDate, { year: 1 }); + 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 }); @@ -283,13 +310,26 @@ function calculateReducer(state: BsDatepickerState): BsDatepickerState { 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 (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 }); @@ -432,20 +472,22 @@ function flagReducer(state: BsDatepickerState): BsDatepickerState { } 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 newState: {view: BsDatepickerViewState} = { view: { mode: state.view.mode, - date - } + date, + source: action.payload.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 ccd4e0ef03..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 @@ -81,6 +84,7 @@ export class BsDatepickerState weekNumbers?: string; unlinkedCalendars?: boolean; + } const _initialView: BsDatepickerViewState = { date: new Date(), mode: 'day' }; diff --git a/src/datepicker/themes/bs/bs-datepicker-view.html b/src/datepicker/themes/bs/bs-datepicker-view.html index 4fbfea672a..2408382692 100644 --- a/src/datepicker/themes/bs/bs-datepicker-view.html +++ b/src/datepicker/themes/bs/bs-datepicker-view.html @@ -9,12 +9,12 @@
@@ -43,10 +43,10 @@
diff --git a/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts index 47b7651495..bbf6acba44 100644 --- a/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts +++ b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts @@ -135,6 +135,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 From 05a3823fc01f9786198aa71b5bde66e6cd61c166 Mon Sep 17 00:00:00 2001 From: Maxie42 Date: Sun, 28 Sep 2025 14:50:40 +0200 Subject: [PATCH 4/8] allow setting mode separetely by calendar --- .../base/bs-datepicker-container.ts | 11 +- src/datepicker/models/index.ts | 2 + .../reducer/bs-datepicker.actions.ts | 9 +- .../reducer/bs-datepicker.effects.ts | 57 +++- .../reducer/bs-datepicker.reducer.ts | 260 ++++++++---------- .../bs/bs-datepicker-container.component.ts | 12 +- ...s-datepicker-inline-container.component.ts | 4 +- .../themes/bs/bs-datepicker-view.html | 60 ++-- .../bs-daterangepicker-container.component.ts | 14 +- ...erangepicker-inline-container.component.ts | 4 +- 10 files changed, 228 insertions(+), 205 deletions(-) diff --git a/src/datepicker/base/bs-datepicker-container.ts b/src/datepicker/base/bs-datepicker-container.ts index 99c11f0af4..f7be9723a6 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 { @@ -76,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(); @@ -93,7 +96,7 @@ 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, source: number): void {} @@ -117,10 +120,10 @@ export abstract class BsDatepickerAbstractComponent { daySelectHandler(day: DayViewModel): 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/models/index.ts b/src/datepicker/models/index.ts index 63724a890e..bbcce4cb6f 100644 --- a/src/datepicker/models/index.ts +++ b/src/datepicker/models/index.ts @@ -53,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 d48ebf6182..7ea8a609db 100644 --- a/src/datepicker/reducer/bs-datepicker.actions.ts +++ b/src/datepicker/reducer/bs-datepicker.actions.ts @@ -8,7 +8,6 @@ import { DatepickerRenderOptions, DatepickerDateCustomClasses, DatepickerDateTooltipText, - BsNavigationDirection } from '../models'; @Injectable({providedIn: 'platform'}) @@ -62,17 +61,17 @@ export class BsDatepickerActions { }; } - 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 } }; } diff --git a/src/datepicker/reducer/bs-datepicker.effects.ts b/src/datepicker/reducer/bs-datepicker.effects.ts index e1ac14d4da..b73a828fec 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, @@ -116,6 +117,7 @@ export class BsDatepickerEffects { return this; } + container.selectedTime = this._store.select(state => state.selectedTime) .pipe(filter(times => !!times)); @@ -142,14 +144,65 @@ 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, source: number): void => { diff --git a/src/datepicker/reducer/bs-datepicker.reducer.ts b/src/datepicker/reducer/bs-datepicker.reducer.ts index da68962c3a..d1a09850c0 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, BsNavigationDirection } 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,23 +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); } @@ -223,62 +222,65 @@ function calculateReducer(state: BsDatepickerState): BsDatepickerState { if (state.viewStates == null) { state.viewStates = new Array(displayMonths); } - if (state.view.mode === 'day' && state.monthViewOptions) { - if (!state.unlinkedCalendars && state.showPreviousMonth && state.selectedRange && state.selectedRange.length === 0) { - viewDate = shiftDate(viewDate, { month: -1 }); + 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; + } } - - state.monthViewOptions.firstDayOfWeek = getLocale(state.locale).firstDayOfWeek(); - let monthsModel = new Array(displayMonths); - for (let monthIndex = 0; monthIndex < displayMonths; monthIndex++) { + 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.unlinkedCalendars && 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[monthIndex].date; - if (monthIndex == source) { + viewDate = state.viewStates[calendarIndex].date; + if (calendarIndex == source) { viewDate = shiftDate(viewDate, { month: state.view.direction }); - state.viewStates[monthIndex] = { date: viewDate, mode: 'day' }; + state.viewStates[calendarIndex] = { date: viewDate, mode: 'day' }; } - monthsModel[monthIndex] = calcDaysCalendar( + monthsModel[calendarIndex] = calcDaysCalendar( viewDate, state.monthViewOptions ); } else { - monthsModel[monthIndex] = calcDaysCalendar( + monthsModel[calendarIndex] = calcDaysCalendar( viewDate, state.monthViewOptions ); - state.viewStates[monthIndex] = { date: viewDate, mode: 'day' }; + 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 (!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); + // 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++ - ) { + if (checkedMode === 'month') { if (source != null && state.unlinkedCalendars) { viewDate = state.viewStates[calendarIndex].date; if (calendarIndex == source) { @@ -298,18 +300,7 @@ function calculateReducer(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++ - ) { + if (checkedMode === 'year') { if (source != null && state.unlinkedCalendars) { viewDate = state.viewStates[calendarIndex].date; if (calendarIndex == source) { @@ -331,39 +322,34 @@ function calculateReducer(state: BsDatepickerState): BsDatepickerState { 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, @@ -372,39 +358,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, @@ -418,18 +403,13 @@ function flagReducer(state: BsDatepickerState): BsDatepickerState { displayMonths, dateCustomClasses: state.dateCustomClasses, dateTooltipTexts: state.dateTooltipTexts, - monthIndex, + monthIndex: idx, unlinkedCalendars: state.unlinkedCalendars, - }) - ); - - return Object.assign({}, state, { flaggedMonths }); - } - - if (state.view.mode === 'month' && state.monthsCalendar) { - const flaggedMonthsCalendar = state.monthsCalendar.map( - (formattedMonth, monthIndex) => - flagMonthsCalendar(formattedMonth, { + }); + } + if (checkedState === 'month' && state.monthsCalendar) { + const formattedMonth = state.monthsCalendar[idx]; + flaggedMonthsCalendar[idx] = flagMonthsCalendar(formattedMonth, { isDisabled: state.isDisabled, minDate: state.minDate, maxDate: state.maxDate, @@ -439,36 +419,29 @@ function flagReducer(state: BsDatepickerState): BsDatepickerState { datesEnabled: state.datesEnabled, selectedRange: state.selectedRange, displayMonths, - monthIndex, - unlinkedCalendars: state.unlinkedCalendars, - }) - ); - - 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, + monthIndex: idx, unlinkedCalendars: state.unlinkedCalendars, - }) - ); - - return Object.assign({}, state, { yearsCalendarFlagged }); + }); + } + 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 { @@ -480,11 +453,12 @@ function navigateOffsetReducer(state: BsDatepickerState, action: Action): BsDate if (!date) { return state; } + const source = action.payload.source; const newState: {view: BsDatepickerViewState} = { view: { - mode: state.view.mode, + mode: source != null && state.viewStates != null ? state.viewStates[source].mode : state.view.mode, date, - source: action.payload.source, + source, direction: action.payload.step['month'] ?? action.payload.step['year'], }, diff --git a/src/datepicker/themes/bs/bs-datepicker-container.component.ts b/src/datepicker/themes/bs/bs-datepicker-container.component.ts index 4d2d4b990a..d6aa673db1 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 ] }) @@ -200,7 +200,7 @@ export class BsDatepickerContainerComponent this._store.dispatch(this._actions.select(day.date)); } - 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 2408382692..958f5a1cca 100644 --- a/src/datepicker/themes/bs/bs-datepicker-view.html +++ b/src/datepicker/themes/bs/bs-datepicker-view.html @@ -4,18 +4,17 @@ [@datepickerAnimation]="animationState" (@datepickerAnimation.done)="positionServiceEnable()"> -
- - -
+
+
+ +
@@ -25,32 +24,29 @@
- - - -
- - -
+ +
+ + +
- -
- - + +
+ + +
diff --git a/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts index bbf6acba44..8f72c99923 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, @@ -221,11 +219,10 @@ export class BsDaterangepickerContainerComponent this.rangesProcessing(day); } - 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') { @@ -239,7 +236,7 @@ export class BsDaterangepickerContainerComponent year: getFullYear(day.date) }, viewMode: 'day' - }) + }, source) ); return; @@ -247,11 +244,10 @@ export class BsDaterangepickerContainerComponent this.rangesProcessing(day); } - 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') { @@ -264,7 +260,7 @@ export class BsDaterangepickerContainerComponent year: getFullYear(day.date) }, viewMode: 'month' - }) + }, source) ); return; 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 { From e1c78bfa5ebe08425a61daae4e60a8d8f4728758 Mon Sep 17 00:00:00 2001 From: Maxie42 Date: Sun, 28 Sep 2025 15:42:55 +0200 Subject: [PATCH 5/8] selecting values in one calendar shouldn't affect the others --- .../base/bs-datepicker-container.ts | 4 ++-- .../reducer/bs-datepicker.actions.ts | 12 +++++----- .../reducer/bs-datepicker.effects.ts | 8 +++---- .../reducer/bs-datepicker.reducer.ts | 23 ++++++++++--------- .../bs/bs-datepicker-container.component.ts | 8 +++---- .../themes/bs/bs-datepicker-view.html | 2 +- .../bs-daterangepicker-container.component.ts | 16 ++++++------- 7 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/datepicker/base/bs-datepicker-container.ts b/src/datepicker/base/bs-datepicker-container.ts index f7be9723a6..1992d92cef 100644 --- a/src/datepicker/base/bs-datepicker-container.ts +++ b/src/datepicker/base/bs-datepicker-container.ts @@ -114,10 +114,10 @@ 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, source: number): void {} diff --git a/src/datepicker/reducer/bs-datepicker.actions.ts b/src/datepicker/reducer/bs-datepicker.actions.ts index 7ea8a609db..1bf9ff09b8 100644 --- a/src/datepicker/reducer/bs-datepicker.actions.ts +++ b/src/datepicker/reducer/bs-datepicker.actions.ts @@ -47,17 +47,17 @@ 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 }, }; } @@ -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 b73a828fec..23ac900b39 100644 --- a/src/datepicker/reducer/bs-datepicker.effects.ts +++ b/src/datepicker/reducer/bs-datepicker.effects.ts @@ -47,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 { diff --git a/src/datepicker/reducer/bs-datepicker.reducer.ts b/src/datepicker/reducer/bs-datepicker.reducer.ts index d1a09850c0..7fb3d743ef 100644 --- a/src/datepicker/reducer/bs-datepicker.reducer.ts +++ b/src/datepicker/reducer/bs-datepicker.reducer.ts @@ -82,10 +82,11 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker if (!state.view) { return state; } - + const source = action.payload.source; const newState = { - selectedDate: action.payload, + selectedDate: action.payload.date, view: state.view, + source: source, }; if (Array.isArray(state.selectedTime)) { @@ -95,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 }; @@ -104,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: { @@ -151,7 +152,7 @@ export function bsDatepickerReducer(state: BsDatepickerState = initialDatepicker } const newState = { - selectedRange: action.payload, + selectedRange: action.payload.value, view: state.view, }; newState.selectedRange?.forEach((dte: Date, index: number) => { @@ -162,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); } diff --git a/src/datepicker/themes/bs/bs-datepicker-container.component.ts b/src/datepicker/themes/bs/bs-datepicker-container.component.ts index d6aa673db1..2ab842929d 100644 --- a/src/datepicker/themes/bs/bs-datepicker-container.component.ts +++ b/src/datepicker/themes/bs/bs-datepicker-container.component.ts @@ -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,7 +197,7 @@ 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, source?: number): void { diff --git a/src/datepicker/themes/bs/bs-datepicker-view.html b/src/datepicker/themes/bs/bs-datepicker-view.html index 958f5a1cca..2159cd6638 100644 --- a/src/datepicker/themes/bs/bs-datepicker-view.html +++ b/src/datepicker/themes/bs/bs-datepicker-view.html @@ -17,7 +17,7 @@ (onViewMode)="setViewMode($event, idx)" (onHover)="dayHoverHandler($event)" (onHoverWeek)="weekHoverHandler($event)" - (onSelect)="daySelectHandler($event)"> + (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 8f72c99923..7124010286 100644 --- a/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts +++ b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts @@ -203,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; } @@ -216,7 +216,7 @@ export class BsDaterangepickerContainerComponent if (isDisabled) { return; } - this.rangesProcessing(day); + this.rangesProcessing(day, source); } override monthSelectHandler(day: CalendarCellViewModel, source?: number): void { @@ -241,7 +241,7 @@ export class BsDaterangepickerContainerComponent return; } - this.rangesProcessing(day); + this.rangesProcessing(day, source); } override yearSelectHandler(day: CalendarCellViewModel, source?: number): void { @@ -265,10 +265,10 @@ export class BsDaterangepickerContainerComponent 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 @@ -291,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 = []; From c6a528f292c760d14a1a7d73e2ab6662123e649b Mon Sep 17 00:00:00 2001 From: Maxie42 Date: Sun, 28 Sep 2025 15:58:01 +0200 Subject: [PATCH 6/8] if has a default value last calendar to the end date when opening --- .../unlinked-calendar-views.component.html | 3 ++- .../unlinked-calendar-views.component.ts | 6 ++++++ src/datepicker/reducer/bs-datepicker.reducer.ts | 5 ++++- src/datepicker/themes/bs/bs-datepicker-view.html | 9 ++++----- 4 files changed, 16 insertions(+), 7 deletions(-) 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 index 465cc5ac85..23898a0584 100644 --- 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 @@ -4,7 +4,8 @@ placeholder="Daterangepicker" class="form-control" bsDaterangepicker - [bsConfig]="{unlinkedCalendars: true}" + [(bsValue)]="dateRangePickerValue" + [bsConfig]="{unlinkedCalendars: true, displayMonths: 2}" >
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 index 5051dd84b0..448f4ea15a 100644 --- 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 @@ -7,4 +7,10 @@ import { Component } from '@angular/core'; standalone: false }) export class UnlinkedCalendarsComponent { + dateRangePickerValue?: (Date | undefined)[]; + range1: Date = new Date(2020, 5, 10); + range2: Date = new Date(2022, 8, 10); + ngOnInit(): void { + this.dateRangePickerValue = [this.range1, this.range2]; + } } diff --git a/src/datepicker/reducer/bs-datepicker.reducer.ts b/src/datepicker/reducer/bs-datepicker.reducer.ts index 7fb3d743ef..a736ca438b 100644 --- a/src/datepicker/reducer/bs-datepicker.reducer.ts +++ b/src/datepicker/reducer/bs-datepicker.reducer.ts @@ -240,7 +240,7 @@ function calculateReducer(state: BsDatepickerState): BsDatepickerState { if (checkedMode === 'day' && state.monthViewOptions != null) { if (calendarIndex == 0) { if (!state.unlinkedCalendars && state.showPreviousMonth && state.selectedRange && state.selectedRange.length === 0) { - viewDate = shiftDate(viewDate, { month: -1 }); + viewDate = shiftDate(viewDate, { month: -1 }); } state.monthViewOptions.firstDayOfWeek = getLocale(state.locale).firstDayOfWeek(); } @@ -255,6 +255,9 @@ function calculateReducer(state: BsDatepickerState): BsDatepickerState { state.monthViewOptions ); } else { + if(calendarIndex == displayMonths -1 && state.unlinkedCalendars && (state.selectedRange ?? []).length == 2) { + viewDate = state.selectedRange![1]; + } monthsModel[calendarIndex] = calcDaysCalendar( viewDate, state.monthViewOptions diff --git a/src/datepicker/themes/bs/bs-datepicker-view.html b/src/datepicker/themes/bs/bs-datepicker-view.html index 2159cd6638..0419909e2a 100644 --- a/src/datepicker/themes/bs/bs-datepicker-view.html +++ b/src/datepicker/themes/bs/bs-datepicker-view.html @@ -20,10 +20,6 @@ (onSelect)="daySelectHandler($event, idx)">
-
- - -
- +
+ + +
From 6469268ed3e8193fa16c6fa1329e217c15a04524 Mon Sep 17 00:00:00 2001 From: Maxie42 Date: Sun, 28 Sep 2025 20:50:05 +0200 Subject: [PATCH 7/8] update existing tests to the new structure --- src/datepicker/base/bs-datepicker-container.ts | 8 ++++---- src/datepicker/engine/flag-days.calendar.spec.ts | 4 ++++ src/datepicker/reducer/bs-datepicker.reducer.ts | 2 +- src/datepicker/testing/bs-datepicker.reducer.spec.ts | 4 ++-- src/datepicker/testing/flaggedMonthsMock.ts | 2 ++ 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/datepicker/base/bs-datepicker-container.ts b/src/datepicker/base/bs-datepicker-container.ts index 1992d92cef..36d38f6c9b 100644 --- a/src/datepicker/base/bs-datepicker-container.ts +++ b/src/datepicker/base/bs-datepicker-container.ts @@ -96,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, source: number): void {} + setViewMode(event: BsDatepickerViewMode, source?: number): void {} // eslint-disable-next-line - navigateTo(event: BsNavigationEvent, source: number): void {} + navigateTo(event: BsNavigationEvent, source?: number): void {} // eslint-disable-next-line dayHoverHandler(event: CellHoverEvent): void {} @@ -120,10 +120,10 @@ export abstract class BsDatepickerAbstractComponent { daySelectHandler(day: DayViewModel, source?: number): void {} // eslint-disable-next-line - monthSelectHandler(event: CalendarCellViewModel, source: number): void {} + monthSelectHandler(event: CalendarCellViewModel, source?: number): void {} // eslint-disable-next-line - yearSelectHandler(event: CalendarCellViewModel, source: number): void {} + yearSelectHandler(event: CalendarCellViewModel, source?: number): void {} // eslint-disable-next-line setRangeOnCalendar(dates: BsCustomDates): void {} 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/reducer/bs-datepicker.reducer.ts b/src/datepicker/reducer/bs-datepicker.reducer.ts index a736ca438b..dffdfcb90a 100644 --- a/src/datepicker/reducer/bs-datepicker.reducer.ts +++ b/src/datepicker/reducer/bs-datepicker.reducer.ts @@ -118,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; 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'), From 1b606f09c8f12b42b85ad9b86f79fa8a351030b4 Mon Sep 17 00:00:00 2001 From: Maxie42 Date: Mon, 29 Sep 2025 00:50:25 +0200 Subject: [PATCH 8/8] add playwright tests for the new feature --- .../src/full/datepicker_page.spec.ts | 65 +++++++++++ .../src/support/datepicker.pw.po.ts | 108 ++++++++++++++++++ .../src/lib/datepicker-section.list.ts | 4 +- .../unlinked-calendar-views.component.ts | 6 +- .../reducer/bs-datepicker.reducer.ts | 2 +- 5 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 apps/ngx-bootstrap-docs-e2e/src/full/datepicker_page.spec.ts create mode 100644 apps/ngx-bootstrap-docs-e2e/src/support/datepicker.pw.po.ts 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/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts b/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts index 98b35770f0..791b3e0950 100644 --- a/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts +++ b/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts @@ -481,7 +481,7 @@ export const demoComponentContent: ContentSection[] = [ outlet: KeepDatesOutOfRulesComponent }, { - title: "Allow separately moving calendars in DateRangePicker", + 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'), @@ -747,7 +747,7 @@ export const demoComponentContent: ContentSection[] = [ outlet: KeepDatesOutOfRulesComponent }, { - title: "Allow separately moving calendars in DateRangePicker", + title: "Unlinked calendars", anchor: 'unlinked-calendar-views', outlet: UnlinkedCalendarsComponent }, 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 index 448f4ea15a..68645d6223 100644 --- 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 @@ -2,14 +2,14 @@ import { Component } from '@angular/core'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector - selector: 'unlinked-calendar-views', + selector: 'datepicker-unlinked-calendar-views', templateUrl: './unlinked-calendar-views.component.html', standalone: false }) export class UnlinkedCalendarsComponent { dateRangePickerValue?: (Date | undefined)[]; - range1: Date = new Date(2020, 5, 10); - range2: Date = new Date(2022, 8, 10); + 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/reducer/bs-datepicker.reducer.ts b/src/datepicker/reducer/bs-datepicker.reducer.ts index dffdfcb90a..5e3ea7bde9 100644 --- a/src/datepicker/reducer/bs-datepicker.reducer.ts +++ b/src/datepicker/reducer/bs-datepicker.reducer.ts @@ -239,7 +239,7 @@ function calculateReducer(state: BsDatepickerState): BsDatepickerState { 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.unlinkedCalendars && state.showPreviousMonth && state.selectedRange && state.selectedRange.length === 0) { + if (state.showPreviousMonth && state.selectedRange && state.selectedRange.length === 0) { viewDate = shiftDate(viewDate, { month: -1 }); } state.monthViewOptions.firstDayOfWeek = getLocale(state.locale).firstDayOfWeek();