Source: argos-sdk/src/Fields/DateField.js

/* Copyright (c) 2010, Sage Software, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import declare from 'dojo/_base/declare';
import event from 'dojo/_base/event';
import string from 'dojo/string';
import format from '../Format';
import FieldManager from '../FieldManager';
import EditorField from './EditorField';
import DateTimePicker from '../DateTimePicker';
import RelativeDateTimePicker from '../RelativeDateTimePicker';
import getResource from '../I18n';


const resource = getResource('dateField');
const dtFormatResource = getResource('dateFieldDateTimeFormat');

/**
 * @class argos.Fields.DateField
 * @classdesc The DateField is an extension of the {@link EditorField EditorField} by accepting Date Objects
 * for values and using the {@link Calendar Calendar} view for user input.
 *
 * @example
 *
 *     {
 *         name: 'StartDate',
 *         property: 'StartDate',
 *         label: this.startDateText,
 *         type: 'date',
 *         dateFormatText: 'MM/DD HH:mm:ss',
 *         showTimerPicker: true,
 *         showRelativeDateTime: true
 *     }
 *
 * @extends argos.Fields.EditorField
 * @requires argos.Calendar
 * @requires argos.FieldManager
 * @requires argos.Format
 */
const control = declare('argos.Fields.DateField', [EditorField], /** @lends argos.Fields.DateField# */{
  // Localization
  /**
   * @cfg {String}
   * The text shown when no value (or null/undefined) is set to the field.
   */
  emptyText: resource.emptyText,
  dateFormatText: dtFormatResource.dateFormatText,
  /**
   * @property {String}
   * The error validation message for this field.
   *
   * `${0}` => Label
   */
  invalidDateFormatErrorText: resource.invalidDateFormatErrorText,

  /**
   * @property {Simplate}
   * Simplate that defines the fields HTML Markup
   *
   * * `$` => Field instance
   * * `$$` => Owner View instance
   *
   */
  widgetTemplate: new Simplate([
    `<label for="{%= $.name %}"
      {% if ($.required) { %}
        class="required"
      {% } %}>
      {%: $.label %}
    </label>
    <div class="field field-control-wrapper">
      <button
        data-dojo-attach-point="triggerNode"
        data-action="showModal"
        class="button field-control-trigger whiteButton"
        aria-label="{%: $.lookupLabelText %}">
          <svg class="icon" focusable="false" aria-hidden="true" role="presentation">
            <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-{%: $.iconClass %}"></use>
          </svg>
        </button>
      <input
        data-dojo-attach-point="inputNode"
        data-dojo-attach-event="onchange:_onChange"
        type="text"
        {% if ($.required) { %}
          data-validate="required"
          class="required"
        {% } %}
        />
    </div>`,
  ]),
  iconClass: 'calendar',

  /**
   * @property {String}
   * The target view id that will provide the user input, this should always be to set to the
   * {@link Calendar Calendars} view id.
   */
  view: 'generic_calendar',
  /**
   * required should be true if the field requires input. Defaults to false.
   * @type {Boolean}
   */
  required: false,
  /**
   * @cfg {Boolean}
   * Sent as part of navigation options to {@link Calendar Calendar}, where it controls the
   * display of the hour/minute inputs.
   */
  showTimePicker: false,
  /**
   * @cfg {Boolean}
   * Sent as part of navigation options to {@link Calendar Calendar}, where it controls the
   * display of the relative date time picker.
   */
  showRelativeDateTime: false,
  /**
   * @cfg {Boolean}
   * Used in formatted and sent as part of navigation options to {@link Calendar Calendar},
   * where it controls the the conversion to/from UTC and setting the hour:min:sec to 00:00:05.
   */
  timeless: false,
  modal: null,
  dateTimePicker: null,
  _modalListener: null,
  /**
   * Takes a date object and calls {@link format#date format.date} passing the current
   * `dateFormatText` and `timeless` values, formatting the date into a string representation.
   * @param {Date} value Date to be converted
   * @return {String}
   */
  formatValue: function formatValue(value) {
    return format.date(value, this.dateFormatText, this.timeless);
  },
  /**
   * When a value changes it checks that the text in the input field matches the defined
   * `dateFormatText` by using it to parse it back into a Date Object. If this succeeds then
   * sets the current value to the Date object and removes any validation warnings. If it
   * doesn't then current value is empties and the validation styling is added.
   * @param {Event} evt Event that caused change to fire.
   */
  _onChange: function _onChange(/* evt*/) {
    const jsDate = new Date(this.inputNode.value);
    let date = moment(this.inputNode.value, this.dateFormatText, true);
    if (moment(jsDate).isValid() && !date.isValid()) {
      date = moment(jsDate);
    }
    const val = date.isValid();

    if (val) {
      this.validationValue = this.currentValue = date.toDate();
      if (this.inputNode.value !== date.format(this.dateFormatText)) {
        this.inputNode.value = date.format(this.dateFormatText);
      }
      $(this.containerNode).removeClass('row-error'); // todo: not the right spot for this, add validation eventing
    } else {
      this.validationValue = this.currentValue = null;
      $(this.containerNode).addClass('row-error'); // todo: not the right spot for this, add validation eventing
    }
    if (this.previousValue !== this.currentValue) {
      this.onChange(this.currentValue, this);
    }
    this.previousValue = this.currentValue;
  },
  /**
   * Extends the parent {@link EditorField#createNavigationOptions createNavigationOptions} to
   * also include the properties `date`, `showTimePicker` and `timeless` with `date` being the current value
   * @return {Object} Navigation options
   */
  createNavigationOptions: function createNavigationOptions() {
    const options = this.inherited(arguments);

    if (this.currentValue !== '' && this.currentValue !== null) {
      options.date = this.currentValue;
    } else {
      options.date = moment();
    }
    options.showTimePicker = this.showTimePicker;
    options.timeless = this.timeless;

    return options;
  },
  /**
   * Retrieves the date from the {@link Calendar#getDateTime Calendar} view and sets it to currentValue.
   */
  getValuesFromView: function getValuesFromView() {
    const view = App.getPrimaryActiveView();
    if (view) {
      this.currentValue = this.validationValue = view.getDateTime();
      $(this.containerNode).removeClass('row-error'); // todo: not the right spot for this, add validation eventing
    }
  },
  getValuesFromModal: function getValuesFromModal(data = {}) {
    if (data.calendar.selectedDateMoment) {
      // This is the case where the DateTimePicker was used to select the date
      const date = data.calendar.selectedDateMoment.clone();
      if (data.time) {
        date.hours(data.time.hours);
        date.minutes(data.time.minutes);
        date.seconds(data.time.seconds);
      }
      this.currentValue = this.validationValue = date.toDate();
      this.inputNode.value = this.formatValue(this.currentValue);
    } else {
      this.currentValue = this.validationValue = data.toDate();
      this.inputNode.value = this.formatValue(this.currentValue);
    }
    $(this.containerNode).removeClass('row-error'); // todo: not the right spot for this, add validation eventing
    this._onChange();
  },
  /**
   * Determines if the current value has been modified from the original value.
   * @return {Boolean}
   */
  isDirty: function isDirty() {
    return this.originalValue instanceof Date && this.currentValue instanceof Date ? this.originalValue.getTime() !== this.currentValue.getTime() : this.originalValue !== this.currentValue;
  },
  /**
   * Extends the parent {@link EditorField#clearValue clearValue} to also include removing the
   * error validation styling.
   */
  clearValue: function clearValue() {
    this.inherited(arguments);
    $(this.containerNode).removeClass('row-error'); // todo: not the right spot for this, add validation eventing
  },
  showModal: function showModal() {
    if (this.isDisabled()) {
      return;
    }

    const options = this.createNavigationOptions();

    let toolbar;
    if (this.showRelativeDateTime && !options.timeless) {
      this.dateTimePicker = new RelativeDateTimePicker({ id: `relative-datetime-picker-modal ${this.id}`, isModal: true });
      toolbar = [
        {
          action: 'cancel',
          className: 'button--flat button--flat--split',
          text: resource.cancelText,
        }, {
          action: this.dateTimePicker.toDateTimePicker,
          className: 'button--flat button--flat--split',
          text: resource.advancedText,
          context: this.dateTimePicker,
        },
      ];
    } else {
      this.dateTimePicker = new DateTimePicker({ id: `datetime-picker-modal ${this.id}`, isModal: true });
      toolbar = [
        {
          action: 'cancel',
          className: 'button--flat button--flat--split',
          text: resource.cancelText,
        }, {
          action: 'resolve',
          className: 'button--flat button--flat--split',
          text: resource.confirmText,
        },
      ];
    }

    App.modal.add(this.dateTimePicker, toolbar, options).then(this.getValuesFromModal.bind(this));
  },
  _onClick: function _onClick(evt) {
    event.stop(evt);
  },
  /**
   * Extends the parent {@link EditorField#validate validate} with a check that makes sure if
   * the user has inputted a date manually into the input field that it had successfully validated
   * in the {@link #_onChange _onChange} function.
   * @return {Boolean/Object} False for no errors. True/Object for invalid.
   */
  validate: function validate() {
    if (this.inputNode.value !== '' && !this.currentValue) {
      return string.substitute(this.invalidDateFormatErrorText, [this.label]);
    }

    return this.inherited(arguments);
  },
});

export default FieldManager.register('date', control);