valor-software/angular2-bootstrap

View on GitHub
src/datepicker/reducer/bs-datepicker.reducer.ts

Summary

Maintainability
F
5 days
Test Coverage
// tslint:disable:max-file-line-count
import { BsDatepickerState, initialDatepickerState } from './bs-datepicker.state';
import { Action } from '../../mini-ngrx/index';
import { BsDatepickerActions } from './bs-datepicker.actions';
import { calcDaysCalendar } from '../engine/calc-days-calendar';
import { formatDaysCalendar } from '../engine/format-days-calendar';
import { flagDaysCalendar } from '../engine/flag-days-calendar';
import { setFullDate, shiftDate } from '../../chronos/utils/date-setters';
import { canSwitchMode } from '../engine/view-mode';
import { formatMonthsCalendar } from '../engine/format-months-calendar';
import { flagMonthsCalendar } from '../engine/flag-months-calendar';
import { formatYearsCalendar, yearsPerCalendar } from '../engine/format-years-calendar';
import { flagYearsCalendar } from '../engine/flag-years-calendar';
import { BsViewNavigationEvent, DatepickerFormatOptions } from '../models/index';
import { isArray, isDateValid } from '../../chronos/utils/type-checks';
import { startOf } from '../../chronos/utils/start-end-of';
import { getLocale } from '../../chronos/locale/locales';
import { isAfter, isBefore } from '../../chronos/utils/date-compare';

export function bsDatepickerReducer(state = initialDatepickerState,
                                    action: Action): BsDatepickerState {
  switch (action.type) {
    case BsDatepickerActions.CALCULATE: {
      return calculateReducer(state);
    }

    case BsDatepickerActions.FORMAT: {
      return formatReducer(state, action);
    }

    case BsDatepickerActions.FLAG: {
      return flagReducer(state, action);
    }

    case BsDatepickerActions.NAVIGATE_OFFSET: {
      const date = shiftDate(startOf(state.view.date, 'month'), action.payload);
      const newState = {
        view: {
          mode: state.view.mode,
          date
        }
      };

      return Object.assign({}, state, newState);
    }

    case BsDatepickerActions.NAVIGATE_TO: {
      const payload: BsViewNavigationEvent = action.payload;

      const date = setFullDate(state.view.date, payload.unit);
      const mode = payload.viewMode;
      const newState = { view: { date, mode } };

      return Object.assign({}, state, newState);
    }

    case BsDatepickerActions.CHANGE_VIEWMODE: {
      if (!canSwitchMode(action.payload)) {
        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: {
      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: {
      const newState = action.payload;
      // preserve view mode
      const mode = 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: {
      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
      });
    }

    default:
      return state;
  }
}

function calculateReducer(state: BsDatepickerState): BsDatepickerState {
  // how many calendars
  const displayMonths = state.displayMonths;
  // use selected date on initial rendering if set
  let viewDate = state.view.date;

  if (state.view.mode === 'day') {
    state.monthViewOptions.firstDayOfWeek = getLocale(state.locale).firstDayOfWeek();
    const 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 });
    }

    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)
      );
      viewDate = shiftDate(viewDate, { year: yearsPerCalendar });
    }

    return Object.assign({}, state, { yearsCalendarModel });
  }

  return state;
}

function formatReducer(state: BsDatepickerState,
                       action: Action): BsDatepickerState {
  if (state.view.mode === 'day') {
    const formattedMonths = state.monthsModel.map((month, monthIndex) =>
      formatDaysCalendar(month, getFormatOptions(state), monthIndex)
    );

    return Object.assign({}, state, { formattedMonths });
  }

  // how many calendars
  const displayMonths = state.displayMonths;
  // 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: BsDatepickerState,
                     action: Action): BsDatepickerState {
  if (state.view.mode === 'day') {
    const flaggedMonths = state.formattedMonths.map(
      (formattedMonth, monthIndex) =>
        flagDaysCalendar(formattedMonth, {
          isDisabled: state.isDisabled,
          minDate: state.minDate,
          maxDate: state.maxDate,
          hoveredDate: state.hoveredDate,
          selectedDate: state.selectedDate,
          selectedRange: state.selectedRange,
          displayMonths: state.displayMonths,
          monthIndex
        })
    );

    return Object.assign({}, state, { flaggedMonths });
  }

  if (state.view.mode === 'month') {
    const flaggedMonthsCalendar = state.monthsCalendar.map(
      (formattedMonth, monthIndex) =>
        flagMonthsCalendar(formattedMonth, {
          isDisabled: state.isDisabled,
          minDate: state.minDate,
          maxDate: state.maxDate,
          hoveredMonth: state.hoveredMonth,
          displayMonths: state.displayMonths,
          monthIndex
        })
    );

    return Object.assign({}, state, { flaggedMonthsCalendar });
  }

  if (state.view.mode === 'year') {
    const yearsCalendarFlagged = state.yearsCalendarModel.map(
      (formattedMonth, yearIndex) =>
        flagYearsCalendar(formattedMonth, {
          isDisabled: state.isDisabled,
          minDate: state.minDate,
          maxDate: state.maxDate,
          hoveredYear: state.hoveredYear,
          displayMonths: state.displayMonths,
          yearIndex
        })
    );

    return Object.assign({}, state, { yearsCalendarFlagged });
  }

  return state;
}

function getFormatOptions(state: BsDatepickerState): DatepickerFormatOptions {
  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: Date | Date[], minDate: Date, maxDate: Date) {
  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;
}