UNPKG

ngx-bootstrap

Version:
1,273 lines (1,254 loc) 174 kB
import { ɵɵdefineInjectable, Injectable, EventEmitter, Component, Renderer2, ElementRef, Directive, ViewContainerRef, Input, Output, forwardRef, Host, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, NgModule } from '@angular/core'; import { filter, map, take, takeUntil, distinctUntilChanged } from 'rxjs/operators'; import { isFirstDayOfWeek, getDay, shiftDate, isBefore, endOf, isAfter, startOf, isArray, isSame, getFirstDayOfMonth, formatDate, getLocale, isSameMonth, isSameDay, isDisabledDay, isSameYear, isDateValid, setFullDate, getMonth, getFullYear, isDate, parseDate, utcAsLocal } from 'ngx-bootstrap/chronos'; import { PositioningService } from 'ngx-bootstrap/positioning'; import { trigger, state, style, transition, animate } from '@angular/animations'; import { Subscription, BehaviorSubject, combineLatest, Subject } from 'rxjs'; import { MiniStore, MiniState } from 'ngx-bootstrap/mini-ngrx'; import { ComponentLoaderFactory } from 'ngx-bootstrap/component-loader'; import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormsModule } from '@angular/forms'; import { isBs3 } from 'ngx-bootstrap/utils'; import { CommonModule } from '@angular/common'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; /** * For date range picker there are `BsDaterangepickerConfig` which inherits all properties, * except `displayMonths`, for range picker it default to `2` */ class BsDatepickerConfig { constructor() { /** sets use adaptive position */ this.adaptivePosition = false; /** sets use UTC date time format */ this.useUtc = false; /** turn on/off animation */ this.isAnimated = false; /** * The view that the datepicker should start in */ this.startView = 'day'; /** * If true, returns focus to the datepicker / daterangepicker input after date selection */ this.returnFocusToInput = false; /** CSS class which will be applied to datepicker container, * usually used to set color theme */ this.containerClass = 'theme-green'; // DatepickerRenderOptions this.displayMonths = 1; /** * Allows to hide week numbers in datepicker */ this.showWeekNumbers = true; this.dateInputFormat = 'L'; // range picker this.rangeSeparator = ' - '; /** * Date format for date range input field */ this.rangeInputFormat = 'L'; // DatepickerFormatOptions this.monthTitle = 'MMMM'; this.yearTitle = 'YYYY'; this.dayLabel = 'D'; this.monthLabel = 'MMMM'; this.yearLabel = 'YYYY'; this.weekNumbers = 'w'; /** * Shows 'today' button */ this.showTodayButton = false; /** * Shows clear button */ this.showClearButton = false; /** * Positioning of 'today' button */ this.todayPosition = 'center'; /** * Positioning of 'clear' button */ this.clearPosition = 'right'; /** * Label for 'today' button */ this.todayButtonLabel = 'Today'; /** * Label for 'clear' button */ this.clearButtonLabel = 'Clear'; /** * Label for 'custom range' button */ this.customRangeButtonLabel = 'Custom Range'; } } BsDatepickerConfig.ɵprov = ɵɵdefineInjectable({ factory: function BsDatepickerConfig_Factory() { return new BsDatepickerConfig(); }, token: BsDatepickerConfig, providedIn: "root" }); BsDatepickerConfig.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; const DATEPICKER_ANIMATION_TIMING = '220ms cubic-bezier(0, 0, 0.2, 1)'; const datepickerAnimation = trigger('datepickerAnimation', [ state('animated-down', style({ height: '*', overflow: 'hidden' })), transition('* => animated-down', [ style({ height: 0, overflow: 'hidden' }), animate(DATEPICKER_ANIMATION_TIMING) ]), state('animated-up', style({ height: '*', overflow: 'hidden' })), transition('* => animated-up', [ style({ height: '*', overflow: 'hidden' }), animate(DATEPICKER_ANIMATION_TIMING) ]), transition('* => unanimated', animate('0s')) ]); class BsDatepickerAbstractComponent { constructor() { this.containerClass = ''; this.customRanges = []; this.chosenRange = []; this._daysCalendarSub = new Subscription(); } set minDate(value) { var _a; (_a = this._effects) === null || _a === void 0 ? void 0 : _a.setMinDate(value); } set maxDate(value) { var _a; (_a = this._effects) === null || _a === void 0 ? void 0 : _a.setMaxDate(value); } set daysDisabled(value) { var _a; (_a = this._effects) === null || _a === void 0 ? void 0 : _a.setDaysDisabled(value); } set datesDisabled(value) { var _a; (_a = this._effects) === null || _a === void 0 ? void 0 : _a.setDatesDisabled(value); } set datesEnabled(value) { var _a; (_a = this._effects) === null || _a === void 0 ? void 0 : _a.setDatesEnabled(value); } set isDisabled(value) { var _a; (_a = this._effects) === null || _a === void 0 ? void 0 : _a.setDisabled(value); } set dateCustomClasses(value) { var _a; (_a = this._effects) === null || _a === void 0 ? void 0 : _a.setDateCustomClasses(value); } set dateTooltipTexts(value) { var _a; (_a = this._effects) === null || _a === void 0 ? void 0 : _a.setDateTooltipTexts(value); } set daysCalendar$(value) { this._daysCalendar$ = value; this._daysCalendarSub.unsubscribe(); this._daysCalendarSub.add(this._daysCalendar$.subscribe(value => { this.multipleCalendars = !!value && value.length > 1; })); } get daysCalendar$() { return this._daysCalendar$; } // todo: valorkin fix // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function setViewMode(event) { } // eslint-disable-next-line navigateTo(event) { } // eslint-disable-next-line dayHoverHandler(event) { } // eslint-disable-next-line weekHoverHandler(event) { } // eslint-disable-next-line monthHoverHandler(event) { } // eslint-disable-next-line yearHoverHandler(event) { } // eslint-disable-next-line daySelectHandler(day) { } // eslint-disable-next-line monthSelectHandler(event) { } // eslint-disable-next-line yearSelectHandler(event) { } // eslint-disable-next-line setRangeOnCalendar(dates) { } // eslint-disable-next-line setToday() { } // eslint-disable-next-line clearDate() { } // eslint-disable-next-line @typescript-eslint/no-explicit-any _stopPropagation(event) { event.stopPropagation(); } } class BsDatepickerActions { calculate() { return { type: BsDatepickerActions.CALCULATE }; } format() { return { type: BsDatepickerActions.FORMAT }; } flag() { return { type: BsDatepickerActions.FLAG }; } select(date) { return { type: BsDatepickerActions.SELECT, payload: date }; } changeViewMode(event) { return { type: BsDatepickerActions.CHANGE_VIEWMODE, payload: event }; } navigateTo(event) { return { type: BsDatepickerActions.NAVIGATE_TO, payload: event }; } navigateStep(step) { return { type: BsDatepickerActions.NAVIGATE_OFFSET, payload: step }; } setOptions(options) { return { type: BsDatepickerActions.SET_OPTIONS, payload: options }; } // date range picker selectRange(value) { return { type: BsDatepickerActions.SELECT_RANGE, payload: value }; } hoverDay(event) { return { type: BsDatepickerActions.HOVER, payload: event.isHovered ? event.cell.date : null }; } minDate(date) { return { type: BsDatepickerActions.SET_MIN_DATE, payload: date }; } maxDate(date) { return { type: BsDatepickerActions.SET_MAX_DATE, payload: date }; } daysDisabled(days) { return { type: BsDatepickerActions.SET_DAYSDISABLED, payload: days }; } datesDisabled(dates) { return { type: BsDatepickerActions.SET_DATESDISABLED, payload: dates }; } datesEnabled(dates) { return { type: BsDatepickerActions.SET_DATESENABLED, payload: dates }; } isDisabled(value) { return { type: BsDatepickerActions.SET_IS_DISABLED, payload: value }; } setDateCustomClasses(value) { return { type: BsDatepickerActions.SET_DATE_CUSTOM_CLASSES, payload: value }; } setDateTooltipTexts(value) { return { type: BsDatepickerActions.SET_DATE_TOOLTIP_TEXTS, payload: value }; } setLocale(locale) { return { type: BsDatepickerActions.SET_LOCALE, payload: locale }; } } BsDatepickerActions.CALCULATE = '[datepicker] calculate dates matrix'; BsDatepickerActions.FORMAT = '[datepicker] format datepicker values'; BsDatepickerActions.FLAG = '[datepicker] set flags'; BsDatepickerActions.SELECT = '[datepicker] select date'; BsDatepickerActions.NAVIGATE_OFFSET = '[datepicker] shift view date'; BsDatepickerActions.NAVIGATE_TO = '[datepicker] change view date'; BsDatepickerActions.SET_OPTIONS = '[datepicker] update render options'; BsDatepickerActions.HOVER = '[datepicker] hover date'; BsDatepickerActions.CHANGE_VIEWMODE = '[datepicker] switch view mode'; BsDatepickerActions.SET_MIN_DATE = '[datepicker] set min date'; BsDatepickerActions.SET_MAX_DATE = '[datepicker] set max date'; BsDatepickerActions.SET_DAYSDISABLED = '[datepicker] set days disabled'; BsDatepickerActions.SET_DATESDISABLED = '[datepicker] set dates disabled'; BsDatepickerActions.SET_DATESENABLED = '[datepicker] set dates enabled'; BsDatepickerActions.SET_IS_DISABLED = '[datepicker] set is disabled'; BsDatepickerActions.SET_DATE_CUSTOM_CLASSES = '[datepicker] set date custom classes'; BsDatepickerActions.SET_DATE_TOOLTIP_TEXTS = '[datepicker] set date tooltip texts'; BsDatepickerActions.SET_LOCALE = '[datepicker] set datepicker locale'; BsDatepickerActions.SELECT_RANGE = '[daterangepicker] select dates range'; BsDatepickerActions.decorators = [ { type: Injectable } ]; class BsLocaleService { constructor() { this._defaultLocale = 'en'; this._locale = new BehaviorSubject(this._defaultLocale); this._localeChange = this._locale.asObservable(); } get locale() { return this._locale; } get localeChange() { return this._localeChange; } get currentLocale() { return this._locale.getValue(); } use(locale) { if (locale === this.currentLocale) { return; } this._locale.next(locale); } } BsLocaleService.decorators = [ { type: Injectable } ]; class BsDatepickerEffects { constructor(_actions, _localeService) { this._actions = _actions; this._localeService = _localeService; this._subs = []; } init(_bsDatepickerStore) { this._store = _bsDatepickerStore; return this; } /** setters */ setValue(value) { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.select(value)); } setRangeValue(value) { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.selectRange(value)); } setMinDate(value) { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.minDate(value)); return this; } setMaxDate(value) { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.maxDate(value)); return this; } setDaysDisabled(value) { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.daysDisabled(value)); return this; } setDatesDisabled(value) { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.datesDisabled(value)); return this; } setDatesEnabled(value) { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.datesEnabled(value)); return this; } setDisabled(value) { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.isDisabled(value)); return this; } setDateCustomClasses(value) { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.setDateCustomClasses(value)); return this; } setDateTooltipTexts(value) { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.setDateTooltipTexts(value)); return this; } /* Set rendering options */ setOptions(_config) { var _a; const _options = Object.assign({ locale: this._localeService.currentLocale }, _config); (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.setOptions(_options)); return this; } /** view to mode bindings */ setBindings(container) { if (!this._store) { return this; } container.daysCalendar$ = this._store.select(state => state.flaggedMonths) .pipe(filter(months => !!months)); // month calendar container.monthsCalendar = this._store.select(state => state.flaggedMonthsCalendar) .pipe(filter(months => !!months)); // year calendar container.yearsCalendar = this._store.select(state => state.yearsCalendarFlagged) .pipe(filter(years => !!years)); container.viewMode = this._store.select(state => { var _a; return (_a = state.view) === null || _a === void 0 ? void 0 : _a.mode; }); container.options$ = combineLatest([ this._store.select(state => state.showWeekNumbers), this._store.select(state => state.displayMonths) ]) .pipe(map((latest) => ({ showWeekNumbers: latest[0], displayMonths: latest[1] }))); return this; } /** event handlers */ setEventHandlers(container) { container.setViewMode = (event) => { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.changeViewMode(event)); }; container.navigateTo = (event) => { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.navigateStep(event.step)); }; container.dayHoverHandler = (event) => { var _a; const _cell = event.cell; if (_cell.isOtherMonth || _cell.isDisabled) { return; } (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.hoverDay(event)); _cell.isHovered = event.isHovered; }; container.monthHoverHandler = (event) => { event.cell.isHovered = event.isHovered; }; container.yearHoverHandler = (event) => { event.cell.isHovered = event.isHovered; }; return this; } registerDatepickerSideEffects() { if (!this._store) { return this; } this._subs.push(this._store.select(state => state.view).subscribe(() => { var _a; (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.calculate()); })); // format calendar values on month model change this._subs.push(this._store .select(state => state.monthsModel) .pipe(filter(monthModel => !!monthModel)) .subscribe(() => { var _a; return (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.format()); })); // flag day values this._subs.push(this._store .select(state => state.formattedMonths) .pipe(filter(month => !!month)) .subscribe(() => { var _a; return (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.flag()); })); // flag day values this._subs.push(this._store .select(state => state.selectedDate) .pipe(filter(selectedDate => !!selectedDate)) .subscribe(() => { var _a; return (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.flag()); })); // flag for date range picker this._subs.push(this._store .select(state => state.selectedRange) .pipe(filter(selectedRange => !!selectedRange)) .subscribe(() => { var _a; return (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.flag()); })); // monthsCalendar this._subs.push(this._store .select(state => state.monthsCalendar) .subscribe(() => { var _a; return (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.flag()); })); // years calendar this._subs.push(this._store .select(state => state.yearsCalendarModel) .pipe(filter(state => !!state)) .subscribe(() => { var _a; return (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.flag()); })); // on hover this._subs.push(this._store .select(state => state.hoveredDate) .pipe(filter(hoveredDate => !!hoveredDate)) .subscribe(() => { var _a; return (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.flag()); })); // date custom classes this._subs.push(this._store .select(state => state.dateCustomClasses) .pipe(filter(dateCustomClasses => !!dateCustomClasses)) .subscribe(() => { var _a; return (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.flag()); })); // date tooltip texts this._subs.push(this._store .select(state => state.dateTooltipTexts) .pipe(filter(dateTooltipTexts => !!dateTooltipTexts)) .subscribe(() => { var _a; return (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.flag()); })); // on locale change this._subs.push(this._localeService.localeChange .subscribe(locale => { var _a; return (_a = this._store) === null || _a === void 0 ? void 0 : _a.dispatch(this._actions.setLocale(locale)); })); return this; } destroy() { for (const sub of this._subs) { sub.unsubscribe(); } } } BsDatepickerEffects.decorators = [ { type: Injectable } ]; BsDatepickerEffects.ctorParameters = () => [ { type: BsDatepickerActions }, { type: BsLocaleService } ]; const defaultMonthOptions = { width: 7, height: 6 }; const dayInMilliseconds = 24 * 60 * 60 * 1000; class BsDatepickerState { constructor() { // DatepickerRenderOptions this.showWeekNumbers = true; this.displayMonths = 1; } } const _initialView = { date: new Date(), mode: 'day' }; const initialDatepickerState = Object.assign(new BsDatepickerConfig(), { locale: 'en', view: _initialView, selectedRange: [], monthViewOptions: defaultMonthOptions }); function getStartingDayOfCalendar(date, options) { if (isFirstDayOfWeek(date, options.firstDayOfWeek)) { return date; } const weekDay = getDay(date); const offset = calculateDateOffset(weekDay, options.firstDayOfWeek); return shiftDate(date, { day: -offset }); } function calculateDateOffset(weekday, startingDayOffset) { const _startingDayOffset = Number(startingDayOffset); if (isNaN(_startingDayOffset)) { return 0; } if (_startingDayOffset === 0) { return weekday; } const offset = weekday - _startingDayOffset % 7; return offset < 0 ? offset + 7 : offset; } function isMonthDisabled(date, min, max) { const minBound = min && isBefore(endOf(date, 'month'), min, 'day'); const maxBound = max && isAfter(startOf(date, 'month'), max, 'day'); return minBound || maxBound || false; } function isYearDisabled(date, min, max) { const minBound = min && isBefore(endOf(date, 'year'), min, 'day'); const maxBound = max && isAfter(startOf(date, 'year'), max, 'day'); return minBound || maxBound || false; } function isDisabledDate(date, datesDisabled) { if (!datesDisabled || !isArray(datesDisabled) || !datesDisabled.length) { return false; } return datesDisabled.some((dateDisabled) => isSame(date, dateDisabled, 'date')); } function isEnabledDate(date, datesEnabled) { if (!datesEnabled || !isArray(datesEnabled) || !datesEnabled.length) { return false; } return !datesEnabled.some((enabledDate) => isSame(date, enabledDate, 'date')); } function getYearsCalendarInitialDate(state, calendarIndex = 0) { const model = state && state.yearsCalendarModel && state.yearsCalendarModel[calendarIndex]; return (model === null || model === void 0 ? void 0 : model.years[0]) && model.years[0][0] && model.years[0][0].date; } function checkRangesWithMaxDate(ranges, maxDate) { if (!ranges) return ranges; if (!maxDate) return ranges; if (!ranges.length && !ranges[0].value) return ranges; ranges.forEach((item) => { if (!item || !item.value) return ranges; if (item.value instanceof Date) return ranges; if (!(item.value instanceof Array && item.value.length)) return ranges; item.value = compareDateWithMaxDateHelper(item.value, maxDate); return ranges; }); return ranges; } function checkBsValue(date, maxDate) { if (!date) return date; if (!maxDate) return date; if (date instanceof Array && !date.length) return date; if (date instanceof Date) return date; return compareDateWithMaxDateHelper(date, maxDate); } function compareDateWithMaxDateHelper(date, maxDate) { if (date instanceof Array) { const editedValues = date.map(item => { if (!item) return item; if (isAfter(item, maxDate, 'date')) item = maxDate; return item; }); return editedValues; } return date; } function createMatrix(options, fn) { let prevValue = options.initialDate; const matrix = new Array(options.height); for (let i = 0; i < options.height; i++) { matrix[i] = new Array(options.width); for (let j = 0; j < options.width; j++) { matrix[i][j] = fn(prevValue); prevValue = shiftDate(prevValue, options.shift); } } return matrix; } // user and model input should handle parsing and validating input values function calcDaysCalendar(startingDate, options) { const firstDay = getFirstDayOfMonth(startingDate); const initialDate = getStartingDayOfCalendar(firstDay, options); // todo test const matrixOptions = { width: options.width || 0, height: options.height || 0, initialDate, shift: { day: 1 } }; const daysMatrix = createMatrix(matrixOptions, date => date); return { daysMatrix, month: firstDay }; } function formatDaysCalendar(daysCalendar, formatOptions, monthIndex) { return { month: daysCalendar.month, monthTitle: formatDate(daysCalendar.month, formatOptions.monthTitle, formatOptions.locale), yearTitle: formatDate(daysCalendar.month, formatOptions.yearTitle, formatOptions.locale), weekNumbers: getWeekNumbers(daysCalendar.daysMatrix, formatOptions.weekNumbers, formatOptions.locale), weekdays: getShiftedWeekdays(formatOptions.locale), weeks: daysCalendar.daysMatrix.map((week, weekIndex) => ({ days: week.map((date, dayIndex) => ({ date, label: formatDate(date, formatOptions.dayLabel, formatOptions.locale), monthIndex, weekIndex, dayIndex })) })), hideLeftArrow: false, hideRightArrow: false, disableLeftArrow: false, disableRightArrow: false }; } function getWeekNumbers(daysMatrix, format, locale) { return daysMatrix.map((days) => (days[0] ? formatDate(days[0], format, locale) : '')); } function getShiftedWeekdays(locale) { const _locale = getLocale(locale); const weekdays = _locale.weekdaysShort(); const firstDayOfWeek = _locale.firstDayOfWeek(); return [...weekdays.slice(firstDayOfWeek), ...weekdays.slice(0, firstDayOfWeek)]; } function flagDaysCalendar(formattedMonth, options) { formattedMonth.weeks.forEach((week) => { week.days.forEach((day, dayIndex) => { // datepicker const isOtherMonth = !isSameMonth(day.date, formattedMonth.month); const isHovered = !isOtherMonth && isSameDay(day.date, options.hoveredDate); // date range picker const isSelectionStart = !isOtherMonth && options.selectedRange && isSameDay(day.date, options.selectedRange[0]); const isSelectionEnd = !isOtherMonth && options.selectedRange && isSameDay(day.date, options.selectedRange[1]); const isSelected = (!isOtherMonth && isSameDay(day.date, options.selectedDate)) || isSelectionStart || isSelectionEnd; const isInRange = !isOtherMonth && options.selectedRange && isDateInRange(day.date, options.selectedRange, options.hoveredDate); const isDisabled = options.isDisabled || isBefore(day.date, options.minDate, 'day') || isAfter(day.date, options.maxDate, 'day') || isDisabledDay(day.date, options.daysDisabled) || isDisabledDate(day.date, options.datesDisabled) || isEnabledDate(day.date, options.datesEnabled); const currentDate = new Date(); const isToday = !isOtherMonth && isSameDay(day.date, currentDate); const customClasses = options.dateCustomClasses && options.dateCustomClasses .map(dcc => isSameDay(day.date, dcc.date) ? dcc.classes : []) .reduce((previousValue, currentValue) => previousValue.concat(currentValue), []) .join(' ') || ''; const tooltipText = options.dateTooltipTexts && options.dateTooltipTexts .map(tt => isSameDay(day.date, tt.date) ? tt.tooltipText : '') .reduce((previousValue, currentValue) => { previousValue.push(currentValue); return previousValue; }, []) .join(' ') || ''; // decide update or not const newDay = Object.assign({}, day, { isOtherMonth, isHovered, isSelected, isSelectionStart, isSelectionEnd, isInRange, isDisabled, isToday, customClasses, tooltipText }); if (day.isOtherMonth !== newDay.isOtherMonth || day.isHovered !== newDay.isHovered || day.isSelected !== newDay.isSelected || day.isSelectionStart !== newDay.isSelectionStart || day.isSelectionEnd !== newDay.isSelectionEnd || day.isDisabled !== newDay.isDisabled || day.isInRange !== newDay.isInRange || day.customClasses !== newDay.customClasses || day.tooltipText !== newDay.tooltipText) { week.days[dayIndex] = newDay; } }); }); // todo: add check for linked calendars formattedMonth.hideLeftArrow = options.isDisabled || (!!options.monthIndex && options.monthIndex > 0 && options.monthIndex !== options.displayMonths); formattedMonth.hideRightArrow = options.isDisabled || ((!!options.monthIndex || options.monthIndex === 0) && !!options.displayMonths && options.monthIndex < options.displayMonths && options.monthIndex + 1 !== options.displayMonths); formattedMonth.disableLeftArrow = isMonthDisabled(shiftDate(formattedMonth.month, { month: -1 }), options.minDate, options.maxDate); formattedMonth.disableRightArrow = isMonthDisabled(shiftDate(formattedMonth.month, { month: 1 }), options.minDate, options.maxDate); return formattedMonth; } function isDateInRange(date, selectedRange, hoveredDate) { if (!date || !selectedRange || !selectedRange[0]) { return false; } if (selectedRange[1]) { return date > selectedRange[0] && date <= selectedRange[1]; } if (hoveredDate) { return date > selectedRange[0] && date <= hoveredDate; } return false; } function canSwitchMode(mode, minMode) { return minMode ? mode >= minMode : true; } const height = 4; const width = 3; const shift = { month: 1 }; function formatMonthsCalendar(viewDate, formatOptions) { const initialDate = startOf(viewDate, 'year'); const matrixOptions = { width, height, initialDate, shift }; const monthMatrix = createMatrix(matrixOptions, date => ({ date, label: formatDate(date, formatOptions.monthLabel, formatOptions.locale) })); return { months: monthMatrix, monthTitle: '', yearTitle: formatDate(viewDate, formatOptions.yearTitle, formatOptions.locale), hideRightArrow: false, hideLeftArrow: false, disableRightArrow: false, disableLeftArrow: false }; } function flagMonthsCalendar(monthCalendar, options) { monthCalendar.months.forEach((months, rowIndex) => { months.forEach((month, monthIndex) => { let isSelected; const isHovered = isSameMonth(month.date, options.hoveredMonth); const isDisabled = options.isDisabled || isMonthDisabled(month.date, options.minDate, options.maxDate); if (!options.selectedDate && options.selectedRange) { isSelected = isSameMonth(month.date, options.selectedRange[0]); if (!isSelected) { isSelected = isSameMonth(month.date, options.selectedRange[1]); } } else { isSelected = isSameMonth(month.date, options.selectedDate); } const newMonth = Object.assign(/*{},*/ month, { isHovered, isDisabled, isSelected }); if (month.isHovered !== newMonth.isHovered || month.isDisabled !== newMonth.isDisabled || month.isSelected !== newMonth.isSelected) { monthCalendar.months[rowIndex][monthIndex] = newMonth; } }); }); // todo: add check for linked calendars monthCalendar.hideLeftArrow = !!options.monthIndex && options.monthIndex > 0 && options.monthIndex !== options.displayMonths; monthCalendar.hideRightArrow = (!!options.monthIndex || options.monthIndex === 0) && (!!options.displayMonths || options.displayMonths === 0) && options.monthIndex < options.displayMonths && options.monthIndex + 1 !== options.displayMonths; monthCalendar.disableLeftArrow = isYearDisabled(shiftDate(monthCalendar.months[0][0].date, { year: -1 }), options.minDate, options.maxDate); monthCalendar.disableRightArrow = isYearDisabled(shiftDate(monthCalendar.months[0][0].date, { year: 1 }), options.minDate, options.maxDate); return monthCalendar; } const height$1 = 4; const width$1 = 4; const yearsPerCalendar = height$1 * width$1; const initialYearShift = (Math.floor(yearsPerCalendar / 2) - 1) * -1; const shift$1 = { year: 1 }; function formatYearsCalendar(viewDate, formatOptions, previousInitialDate) { const initialDate = calculateInitialDate(viewDate, previousInitialDate); const matrixOptions = { width: width$1, height: height$1, initialDate, shift: shift$1 }; const yearsMatrix = createMatrix(matrixOptions, date => ({ date, label: formatDate(date, formatOptions.yearLabel, formatOptions.locale) })); const yearTitle = formatYearRangeTitle(yearsMatrix, formatOptions); return { years: yearsMatrix, monthTitle: '', yearTitle, hideLeftArrow: false, hideRightArrow: false, disableLeftArrow: false, disableRightArrow: false }; } function calculateInitialDate(viewDate, previousInitialDate) { if (previousInitialDate && viewDate.getFullYear() >= previousInitialDate.getFullYear() && viewDate.getFullYear() < previousInitialDate.getFullYear() + yearsPerCalendar) { return previousInitialDate; } return shiftDate(viewDate, { year: initialYearShift }); } function formatYearRangeTitle(yearsMatrix, formatOptions) { const from = formatDate(yearsMatrix[0][0].date, formatOptions.yearTitle, formatOptions.locale); const to = formatDate(yearsMatrix[height$1 - 1][width$1 - 1].date, formatOptions.yearTitle, formatOptions.locale); return `${from} - ${to}`; } function flagYearsCalendar(yearsCalendar, options) { yearsCalendar.years.forEach((years, rowIndex) => { years.forEach((year, yearIndex) => { let isSelected; const isHovered = isSameYear(year.date, options.hoveredYear); const isDisabled = options.isDisabled || isYearDisabled(year.date, options.minDate, options.maxDate); if (!options.selectedDate && options.selectedRange) { isSelected = isSameYear(year.date, options.selectedRange[0]); if (!isSelected) { isSelected = isSameYear(year.date, options.selectedRange[1]); } } else { isSelected = isSameYear(year.date, options.selectedDate); } const newMonth = Object.assign(/*{},*/ year, { isHovered, isDisabled, isSelected }); if (year.isHovered !== newMonth.isHovered || year.isDisabled !== newMonth.isDisabled || year.isSelected !== newMonth.isSelected) { yearsCalendar.years[rowIndex][yearIndex] = newMonth; } }); }); // todo: add check for linked calendars yearsCalendar.hideLeftArrow = !!options.yearIndex && options.yearIndex > 0 && options.yearIndex !== options.displayMonths; yearsCalendar.hideRightArrow = !!options.yearIndex && !!options.displayMonths && options.yearIndex < options.displayMonths && options.yearIndex + 1 !== options.displayMonths; yearsCalendar.disableLeftArrow = isYearDisabled(shiftDate(yearsCalendar.years[0][0].date, { year: -1 }), options.minDate, options.maxDate); const i = yearsCalendar.years.length - 1; const j = yearsCalendar.years[i].length - 1; yearsCalendar.disableRightArrow = isYearDisabled(shiftDate(yearsCalendar.years[i][j].date, { year: 1 }), options.minDate, options.maxDate); return yearsCalendar; } function bsDatepickerReducer(state = initialDatepickerState, action) { switch (action.type) { case BsDatepickerActions.CALCULATE: { return calculateReducer(state); } case BsDatepickerActions.FORMAT: { return formatReducer(state); } case BsDatepickerActions.FLAG: { return flagReducer(state); } case BsDatepickerActions.NAVIGATE_OFFSET: { return navigateOffsetReducer(state, action); } case BsDatepickerActions.NAVIGATE_TO: { const payload = action.payload; if (!state.view || !payload.unit) { return state; } const date = setFullDate(state.view.date, payload.unit); let newState; let mode; if (canSwitchMode(payload.viewMode, state.minMode)) { mode = payload.viewMode; newState = { view: { date, mode } }; } else { mode = state.view.mode; newState = { selectedDate: date, view: { date, mode } }; } return Object.assign({}, state, newState); } case BsDatepickerActions.CHANGE_VIEWMODE: { if (!canSwitchMode(action.payload, state.minMode) || !state.view) { return state; } const date = state.view.date; const mode = action.payload; const newState = { view: { date, mode } }; return Object.assign({}, state, newState); } case BsDatepickerActions.HOVER: { return Object.assign({}, state, { hoveredDate: action.payload }); } case BsDatepickerActions.SELECT: { if (!state.view) { return state; } const newState = { selectedDate: action.payload, view: state.view }; const mode = state.view.mode; const _date = action.payload || state.view.date; const date = getViewDate(_date, state.minDate, state.maxDate); newState.view = { mode, date }; return Object.assign({}, state, newState); } case BsDatepickerActions.SET_OPTIONS: { if (!state.view) { return state; } const newState = action.payload; // preserve view mode const mode = newState.minMode ? newState.minMode : state.view.mode; const _viewDate = isDateValid(newState.value) && newState.value || isArray(newState.value) && isDateValid(newState.value[0]) && newState.value[0] || state.view.date; const date = getViewDate(_viewDate, newState.minDate, newState.maxDate); newState.view = { mode, date }; // update selected value if (newState.value) { // if new value is array we work with date range if (isArray(newState.value)) { newState.selectedRange = newState.value; } // if new value is a date -> datepicker if (newState.value instanceof Date) { newState.selectedDate = newState.value; } // provided value is not supported :) // need to report it somehow } return Object.assign({}, state, newState); } // date range picker case BsDatepickerActions.SELECT_RANGE: { if (!state.view) { return state; } const newState = { selectedRange: action.payload, view: state.view }; const mode = state.view.mode; const _date = action.payload && action.payload[0] || state.view.date; const date = getViewDate(_date, state.minDate, state.maxDate); newState.view = { mode, date }; return Object.assign({}, state, newState); } case BsDatepickerActions.SET_MIN_DATE: { return Object.assign({}, state, { minDate: action.payload }); } case BsDatepickerActions.SET_MAX_DATE: { return Object.assign({}, state, { maxDate: action.payload }); } case BsDatepickerActions.SET_IS_DISABLED: { return Object.assign({}, state, { isDisabled: action.payload }); } case BsDatepickerActions.SET_DATE_CUSTOM_CLASSES: { return Object.assign({}, state, { dateCustomClasses: action.payload }); } case BsDatepickerActions.SET_DATE_TOOLTIP_TEXTS: { return Object.assign({}, state, { dateTooltipTexts: action.payload }); } default: return state; } } function calculateReducer(state) { if (!state.view) { return state; } // how many calendars let displayMonths; if (state.displayOneMonthRange && isDisplayOneMonth(state.view.date, state.minDate, state.maxDate)) { displayMonths = 1; } else { displayMonths = state.displayMonths || 1; } // 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 }); } // 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); } } 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 }); } 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 }); } return Object.assign({}, state, { yearsCalendarModel }); } return state; } function formatReducer(state) { 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++) { // todo: for unlinked calendars it will be harder monthsCalendar[calendarIndex] = formatMonthsCalendar(viewDate, getFormatOptions(state)); 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)); viewDate = shiftDate(viewDate, { year: 16 }); } return Object.assign({}, state, { yearsCalendarModel }); } return state; } function flagReducer(state) { 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) => flagDaysCalendar(formattedMonth, { isDisabled: state.isDisabled, minDate: state.minDate, maxDate: state.maxDate, daysDisabled: state.daysDisabled, datesDisabled: state.datesDisabled, datesEnabled: state.datesEnabled, hoveredDate: state.hoveredDate, selectedDate: state.selectedDate, selectedRange: state.selectedRange, 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, { isDisabled: state.isDisabled, minDate: state.minDate, maxDate: state.maxDate, hoveredMonth: state.hoveredMonth, selectedDate: state.selectedDate, 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, selectedRange: state.selectedRange, displayMonths, yearIndex })); return Object.assign({}, state, { yearsCalendarFlagged }); } return state; } function navigateOffsetReducer(state, action) { if (!state.view) { return state; } const date = shiftViewDate(state, action); if (!date) { return state; } const newState = { view: { mode: state.view.mode, date } }; return Object.assign({}, state, newState); } function shiftViewDate(state, action) { if (!state.view) { return undefined; } if (state.view.mode === 'year' && state.minMode === 'year') { const initialDate = getYearsCalendarInitialDate(state, 0); if (initialDate) { const middleDate = shiftDate(initialDate, { year: -initialYearShift }); return shiftDate(middleDate, action.payload); } } return shiftDate(startOf(state.view.date, 'month'), action.payload); } function getFormatOptions(state) { return { locale: state.locale, monthTitle: state.monthTitle, yearTitle: state.yearTitle, dayLabel: state.dayLabel, monthLabel: state.monthLabel, yearLabel: state.yearLabel, weekNumbers: state.weekNumbers }; } /** * if view date is provided (bsValue|ngModel) it should be shown * if view date is not provider: * if minDate>currentDate (default view value), show minDate * if maxDate<currentDate(default view value) show maxDate */ function getViewDate(viewDate, minDate, maxDate) { const _date = Array.isArray(viewDate) ? viewDate[0] : viewDate; if (minDate && isAfter(minDate, _date, 'day')) { return minDate; } if (maxDate && isBefore(maxDate, _date, 'day')) { return maxDate; } return _date; } function isDisplayOneMonth(viewDate, minDate, maxDate) { if (maxDate && isSame(maxDate,