Home Reference Source

src/components/day/Day.js

import _ from 'lodash';
import Field from '../_classes/field/Field';
import { boolValue, getLocaleDateFormatInfo } from '../../utils/utils';

export default class DayComponent extends Field {
  static schema(...extend) {
    return Field.schema({
      type: 'day',
      label: 'Day',
      key: 'day',
      fields: {
        day: {
          type: 'number',
          placeholder: '',
          required: false
        },
        month: {
          type: 'select',
          placeholder: '',
          required: false
        },
        year: {
          type: 'number',
          placeholder: '',
          required: false
        }
      },
      dayFirst: false
    }, ...extend);
  }

  static get builderInfo() {
    return {
      title: 'Day',
      group: 'advanced',
      icon: 'calendar',
      documentation: '/userguide/#day',
      weight: 50,
      schema: DayComponent.schema()
    };
  }

  /**
   * The empty value for day component.
   *
   * @return {'00/00/0000'}
   */
  get emptyValue() {
    return '00/00/0000';
  }

  get valueMask() {
    return /^\d{2}\/\d{2}\/\d{4}$/;
  }

  get dayRequired() {
    return this.showDay && _.get(this.component, 'fields.day.required', false);
  }

  get showDay() {
    return !_.get(this.component, 'fields.day.hide', false);
  }

  get monthRequired() {
    return this.showMonth && _.get(this.component, 'fields.month.required', false);
  }

  get showMonth() {
    return !_.get(this.component, 'fields.month.hide', false);
  }

  get yearRequired() {
    return this.showYear && _.get(this.component, 'fields.year.required', false);
  }

  get showYear() {
    return !_.get(this.component, 'fields.year.hide', false);
  }

  get defaultSchema() {
    return DayComponent.schema();
  }

  get shouldDisabled() {
    return super.shouldDisabled || this.parentDisabled;
  }

  get inputInfo() {
    const info = super.elementInfo();
    info.type = 'input';
    info.attr.type = 'hidden';
    info.changeEvent = 'input';
    return info;
  }

  inputDefinition(name) {
    let min, max;
    if (name === 'day') {
      min = 1;
      max = 31;
    }
    if (name === 'month') {
      min = 1;
      max = 12;
    }
    if (name === 'year') {
      min = _.get(this.component, 'fields.year.minYear', 1900) || 1900;
      max = _.get(this.component, 'fields.year.maxYear', 2030) || 1900;
    }
    return {
      type: 'input',
      ref: name,
      attr: {
        id: `${this.component.key}-${name}`,
        class: `form-control ${this.transform('class', `formio-day-component-${name}`)}`,
        type: this.component.fields[name].type === 'select' ? 'select' : 'number',
        placeholder: this.component.fields[name].placeholder,
        step: 1,
        min,
        max,
      }
    };
  }

  selectDefinition(name) {
    return {
      multiple: false,
      ref: name,
      widget: 'html5',
      attr: {
        id: `${this.component.key}-${name}`,
        class: 'form-control',
        name,
        lang: this.options.language
      }
    };
  }

  get days() {
    if (this._days) {
      return this._days;
    }
    this._days = [
      { value: '', label: _.get(this.component, 'fields.day.placeholder', '') }
    ];
    for (let x = 1; x <= 31; x++) {
      this._days.push({
        value: x,
        label: x.toString()
      });
    }
    return this._days;
  }

  get months() {
    if (this._months) {
      return this._months;
    }
    this._months = [
      {
        value: '',
        label: _.get(this.component, 'fields.month.placeholder') || (this.hideInputLabels ? this.t('Month') : '')
      },
      { value: 1, label: 'January' },
      { value: 2, label: 'February' },
      { value: 3, label: 'March' },
      { value: 4, label: 'April' },
      { value: 5, label: 'May' },
      { value: 6, label: 'June' },
      { value: 7, label: 'July' },
      { value: 8, label: 'August' },
      { value: 9, label: 'September' },
      { value: 10, label: 'October' },
      { value: 11, label: 'November' },
      { value: 12, label: 'December' }
    ];
    return this._months;
  }

  get years() {
    if (this._years) {
      return this._years;
    }
    this._years = [
      { value: '', label: _.get(this.component, 'fields.year.placeholder', '') }
    ];
    const minYears = _.get(this.component, 'fields.year.minYear', 1900) || 1900;
    const maxYears = _.get(this.component, 'fields.year.maxYear', 2030) || 2030;
    for (let x = minYears; x <= maxYears; x++) {
      this._years.push({
        value: x,
        label: x.toString()
      });
    }
    return this._years;
  }

  setErrorClasses(elements, dirty, hasError) {
    super.setErrorClasses(elements, dirty, hasError);
    super.setErrorClasses([this.refs.day, this.refs.month, this.refs.year], dirty, hasError);
  }

  removeInputError(elements) {
    super.removeInputError([this.refs.day, this.refs.month, this.refs.year]);
    super.removeInputError(elements);
  }

  init() {
    super.init();
    this.validators = this.validators.concat(['day', 'maxDate', 'minDate', 'minYear', 'maxYear']);

    const minYear = this.component.fields.year.minYear;
    const maxYear = this.component.fields.year.maxYear;
    this.component.maxYear = maxYear;
    this.component.minYear = minYear;

    const dateFormatInfo = getLocaleDateFormatInfo(this.options.language);
    this.dayFirst = this.component.useLocaleSettings
      ? dateFormatInfo.dayFirst
      : this.component.dayFirst;
  }

  render() {
    return super.render(this.renderTemplate('day', {
      dayFirst: this.dayFirst,
      showDay: this.showDay,
      showMonth: this.showMonth,
      showYear: this.showYear,
      day: this.renderField('day'),
      month: this.renderField('month'),
      year: this.renderField('year'),
    }));
  }

  renderField(name) {
    if (this.component.fields[name].type === 'select') {
      return this.renderTemplate('select', {
        input: this.selectDefinition(name),
        selectOptions: this[`${name}s`].reduce((html, option) =>
          html + this.renderTemplate('selectOption', {
            option,
            selected: false,
            attrs: {}
          }), ''
        ),
      });
    }
    else {
      return this.renderTemplate('input', {
        prefix: this.prefix,
        suffix: this.suffix,
        input: this.inputDefinition(name)
      });
    }
  }

  attach(element) {
    this.loadRefs(element, { day: 'single', month: 'single', year: 'single', input: 'multiple' });
    const superAttach = super.attach(element);
    if (this.shouldDisabled) {
      this.setDisabled(this.refs.day, true);
      this.setDisabled(this.refs.month, true);
      this.setDisabled(this.refs.year, true);
      if (this.refs.input) {
        this.refs.input.forEach((input) => this.setDisabled(input, true));
      }
    }
    else {
      this.addEventListener(this.refs.day, 'input', () => this.updateValue(null, {
        modified: true
      }));
      // TODO: Need to rework this to work with day select as well.
      // Change day max input when month changes.
      this.addEventListener(this.refs.month, 'input', () => {
        const maxDay = this.refs.year ? parseInt(new Date(this.refs.year.value, this.refs.month.value, 0).getDate(), 10)
          : '';
        const day = this.getFieldValue('day');
        if (!this.component.fields.day.hide && maxDay) {
          this.refs.day.max = maxDay;
        }
        if (maxDay && day > maxDay) {
          this.refs.day.value = this.refs.day.max;
        }
        this.updateValue(null, {
          modified: true
        });
      });
      this.addEventListener(this.refs.year, 'input', () => this.updateValue(null, {
        modified: true
      }));
      this.addEventListener(this.refs.input, this.info.changeEvent, () => this.updateValue(null, {
        modified: true
      }));
    }
    this.setValue(this.dataValue);
    // Force the disabled state with getters and setters.
    this.disabled = this.shouldDisabled;
    return superAttach;
  }

  validateRequired(setting, value) {
    const { day, month, year } = this.parts;
    if (this.dayRequired && !day) {
      return false;
    }

    if (this.monthRequired && !month) {
      return false;
    }

    if (this.yearRequired && !year) {
      return false;
    }

    if (!boolValue(setting)) {
      return true;
    }
    return !this.isEmpty(value);
  }

  set disabled(disabled) {
    super.disabled = disabled;
    if (!this.refs.year || !this.refs.month || !this.refs.day) {
      return;
    }
    if (disabled) {
      this.refs.year.setAttribute('disabled', 'disabled');
      this.refs.month.setAttribute('disabled', 'disabled');
      this.refs.day.setAttribute('disabled', 'disabled');
    }
    else {
      this.refs.year.removeAttribute('disabled');
      this.refs.month.removeAttribute('disabled');
      this.refs.day.removeAttribute('disabled');
    }
  }

  normalizeValue(value) {
    if (!value || this.valueMask.test(value)) {
      return value;
    }
    const dateParts = [];
    const valueParts = value.split('/');

    const getNextPart = (shouldTake, defaultValue) =>
      dateParts.push(shouldTake ? valueParts.shift() : defaultValue);

    if (this.dayFirst) {
      getNextPart(this.showDay, '00');
    }

    getNextPart(this.showMonth, '00');

    if (!this.dayFirst) {
      getNextPart(this.showDay, '00');
    }

    getNextPart(this.showYear, '0000');

    return dateParts.join('/');
  }

  /**
   * Set the value at a specific index.
   *
   * @param index
   * @param value
   */
  setValueAt(index, value) {
    // temporary solution to avoid input reset
    // on invalid date.
    if (!value || value === 'Invalid date') {
      return null;
    }
    const parts = value.split('/');
    let day;
    if (this.component.dayFirst) {
      day = parts.shift();
    }
    const month = parts.shift();
    if (!this.component.dayFirst) {
      day = parts.shift();
    }
    const year = parts.shift();

    if (this.refs.day && this.showDay) {
      this.refs.day.value = day === '00' ? '' : parseInt(day, 10);
    }
    if (this.refs.month && this.showMonth) {
      this.refs.month.value = month === '00' ? '' : parseInt(month, 10);
    }
    if (this.refs.year && this.showYear) {
      this.refs.year.value = year === '0000' ? '' : parseInt(year, 10);
    }
  }

  getFieldValue(name) {
    const parts = this.dataValue ? this.dataValue.split('/') : [];
    let val = 0;

    switch (name) {
      case 'month':
        val = parts[this.dayFirst ? 1 : 0];
        break;
      case 'day':
        val = parts[this.dayFirst ? 0 : 1];
        break;
      case 'year':
        val = parts[2];
        break;
    }

    val = parseInt(val, 10);
    return (!_.isNaN(val) && _.isNumber(val)) ? val : 0;
  }

  get parts() {
    return {
      day: this.getFieldValue('day'),
      month: this.getFieldValue('month'),
      year: this.getFieldValue('year'),
    };
  }

  /**
   * Get the format for the value string.
   * @returns {string}
   */
  get format() {
    let format = '';
    if (this.component.dayFirst && this.showDay) {
      format += 'D/';
    }
    if (this.showMonth) {
      format += 'M/';
    }
    if (!this.component.dayFirst && this.showDay) {
      format += 'D/';
    }
    if (this.showYear) {
      format += 'YYYY';
      return format;
    }
    else {
      // Trim off the "/" from the end of the format string.
      return format.length ? format.substring(0, format.length - 1) : format;
    }
  }

  /**
   * Return the date for this component.
   *
   * @param value
   * @return {*}
   */
  getDate(value) {
    let defaults = [], day, month, year;
    // Map positions to identifiers to get default values for each part of day
    const [DAY, MONTH, YEAR] = this.component.dayFirst ? [0, 1, 2] : [1, 0, 2];
    const defaultValue = value || this.component.defaultValue;
    if (defaultValue) {
      defaults = defaultValue.split('/').map(x => parseInt(x, 10));
    }

    if (this.showDay && this.refs.day) {
      day = parseInt(this.refs.day.value, 10);
    }
    if (day === undefined || _.isNaN(day)) {
      day = defaults[DAY] && !_.isNaN(defaults[DAY]) ? defaults[DAY] : 0;
    }

    if (this.showMonth && this.refs.month) {
      // Months are 0 indexed.
      month = parseInt(this.refs.month.value, 10);
    }
    if (month === undefined || _.isNaN(month)) {
      month = defaults[MONTH] && !_.isNaN(defaults[MONTH]) ? defaults[MONTH] : 0;
    }

    if (this.showYear && this.refs.year) {
      year = parseInt(this.refs.year.value);
    }
    if (year === undefined || _.isNaN(year)) {
      year = defaults[YEAR] && !_.isNaN(defaults[YEAR]) ? defaults[YEAR] : 0;
    }

    let result;
    if (!day && !month && !year) {
      return null;
    }

    // add trailing zeros if the data is showed
    day = this.showDay ? day.toString().padStart(2, 0) : '';
    month = this.showMonth ? month.toString().padStart(2, 0) : '';
    year = this.showYear ? year.toString().padStart(4, 0) : '';

    if (this.component.dayFirst) {
      result = `${day}${this.showDay && this.showMonth || this.showDay && this.showYear ? '/' : ''}${month}${this.showMonth && this.showYear ? '/' : ''}${year}`;
    }
    else {
      result = `${month}${this.showDay && this.showMonth || this.showMonth && this.showYear ? '/' : ''}${day}${this.showDay && this.showYear ? '/' : ''}${year}`;
    }

    return result;
  }

  /**
   * Return the date object for this component.
   * @returns {Date}
   */
  get date() {
    return this.getDate();
  }

  normalizeMinMaxDates() {
   return [this.component.minDate, this.component.maxDate]
      .map(date => date ? date.split('-').reverse().join('/') : date);
  }

  /**
   * Return the raw value.
   *
   * @returns {Date}
   */
  get validationValue() {
    [this.component.minDate, this.component.maxDate] = this.dayFirst ? this.normalizeMinMaxDates()
      : [this.component.minDate, this.component.maxDate];
    return this.dataValue;
  }

  getValue() {
    const result = super.getValue();
    return (!result) ? this.dataValue : result;
  }

  /**
   * Get the value at a specific index.
   *
   * @param index
   * @returns {*}
   */
  getValueAt(index) {
    const date = this.date;
    if (date) {
      this.refs.input[index].value = date;
      return this.refs.input[index].value;
    }
    else {
      this.refs.input[index].value = '';
      return null;
    }
  }

  /**
   * Get the input value of the date.
   *
   * @param value
   * @return {null}
   */
  getValueAsString(value) {
    return this.getDate(value) || '';
  }

  focus() {
    if (this.dayFirst && this.showDay || !this.dayFirst && !this.showMonth && this.showDay) {
      this.refs.day.focus();
    }
    else if (this.dayFirst && !this.showDay && this.showMonth || !this.dayFirst && this.showMonth) {
      this.refs.month.focus();
    }
    else if (!this.showDay && !this.showDay && this.showYear) {
      this.refs.year.focus();
    }
  }

  isPartialDay(value) {
    if (!value) {
      return false;
    }
    const [DAY, MONTH, YEAR] = this.component.dayFirst ? [0, 1, 2] : [1, 0, 2];
    const values = value.split('/');
    return (values[DAY] === '00' || values[MONTH] === '00' || values[YEAR] === '0000');
  }

  getValidationFormat() {
    return this.dayFirst ? 'DD-MM-YYYY' : 'MM-DD-YYYY';
  }
}