vuematerial/vue-material

View on GitHub
src/components/MdDatepicker/MdDatepicker.vue

Summary

Maintainability
Test Coverage
F
34%
<template>
  <md-field :class="['md-datepicker', { 'md-native': !mdOverrideNative }]" :md-clearable="mdClearable" @md-clear="onClear">
    <md-date-icon class="md-date-icon" @click.native="toggleDialog(true)" />
    <md-input :type="type" ref="input" v-model="inputDate" @focus.native="onFocus" @focusout.native="onFocusOut" :pattern="pattern" />

    <slot />

    <keep-alive>
      <md-datepicker-dialog
        v-if="showDialog"
        ref="mdRef"
        :md-date.sync="localDate"
        :md-disabled-dates="mdDisabledDates"
        :mdImmediately="mdImmediately"
        @md-closed="toggleDialog(false)"
        :md-placement="mdPlacement"
      />
    </keep-alive>

    <md-overlay class="md-datepicker-overlay" md-fixed :md-active="showDialog" @click="toggleDialog(false)" />
  </md-field>
</template>

<script>
  import Vue from 'vue'
  import isFirefox from 'is-firefox'
  import format from 'date-fns/format'
  import parse from 'date-fns/parse'
  import isValid from 'date-fns/isValid'
  import MdPropValidator from 'core/utils/MdPropValidator'
  import MdOverlay from 'components/MdOverlay/MdOverlay'
  import MdDatepickerDialog from './MdDatepickerDialog'
  import MdDateIcon from 'core/icons/MdDateIcon'
  import MdDebounce from 'core/utils/MdDebounce'
  import MdField from 'components/MdField/MdField'
  import MdInput from 'components/MdField/MdInput/MdInput'

  export default {
    name: 'MdDatepicker',
    components: {
      MdOverlay,
      MdDateIcon,
      MdField,
      MdInput,
      MdDatepickerDialog
    },
    props: {
      value: [String, Number, Date],
      mdDisabledDates: [Array, Function],
      mdOpenOnFocus: {
        type: Boolean,
        default: true
      },
      mdCloseOnBlur: {
        type: Boolean,
        default: true
      },
      mdOverrideNative: {
        type: Boolean,
        default: true
      },
      mdImmediately: {
        type: Boolean,
        default: false
      },
      mdModelType: {
        type: Function,
        default: Date,
        ...MdPropValidator('md-model-type', [Date, String, Number])
      },
      MdDebounce: {
        type: Number,
        default: 1000
      },
      mdClearable: {
        type: Boolean,
        default: true
      },
      mdPlacement: {
        type: String,
        default: 'bottom-start'
      }
    },
    data: () => ({
      showDialog: false,
      // String for input
      inputDate: '',
      // Date for real value
      localDate: null
    }),
    computed: {
      locale () {
        return this.$material.locale
      },
      type () {
        return this.mdOverrideNative
          ? 'text'
          : 'date'
      },
      dateFormat () {
        return this.locale.dateFormat || 'yyyy-MM-dd'
      },
      modelType () {
        if (this.isModelTypeString) {
          return String
        } else if (this.isModelTypeNumber) {
          return Number
        } else if (this.isModelTypeDate) {
          return Date
        } else {
          return this.mdModelType
        }
      },
      isModelNull () {
        return this.value === null || this.value === undefined
      },
      isModelTypeString () {
        return typeof this.value === 'string'
      },
      isModelTypeNumber () {
        return Number.isInteger(this.value) && this.value >= 0
      },
      isModelTypeDate () {
        return typeof this.value === 'object' && this.value instanceof Date && isValid(this.value)
      },
      localString () {
        return this.localDate && format(this.localDate, this.dateFormat)
      },
      localNumber () {
        return this.localDate && Number(this.localDate)
      },
      parsedInputDate () {
        const parsedDate = parse(this.inputDate, this.dateFormat, new Date())
        return parsedDate && isValid(parsedDate) ? parsedDate : null
      },
      pattern () {
        return this.dateFormat.replace(/yyyy|MM|dd/g, match => {
          switch (match) {
          case 'yyyy':
            return '[0-9]{4}'
          case 'MM':
          case 'dd':
            return '[0-9]{2}'
          }
        })
      }
    },
    watch: {
      inputDate () {
        this.inputDateToLocalDate()
      },
      localDate () {
        this.inputDate = this.localString
        if (this.modelType === Date) {
          this.$emit('input', this.localDate)
        }
      },
      localString () {
        if (this.modelType === String) {
          this.$emit('input', this.localString)
        }
      },
      localNumber () {
        if (this.modelType === Number) {
          this.$emit('input', this.localNumber)
        }
      },
      value: {
        immediate: true,
        handler () {
          this.valueDateToLocalDate()
        }
      },
      mdModelType (type) {
        switch (type) {
        case Date:
          this.$emit('input', this.localDate)
          break
        case String:
          this.$emit('input', this.localString)
          break
        case Number:
          this.$emit('input', this.localNumber)
          break
        }
      },
      dateFormat () {
        if (this.localDate) {
          this.inputDate = format(this.localDate, this.dateFormat)
        }
      }
    },
    methods: {
      toggleDialog (newState = null) {
        if (!isFirefox || this.mdOverrideNative) {
          // If new state (boolean) is provide, assign that to showDialog, else just toggle
          this.showDialog = newState === null ? !this.showDialog : newState
          if (this.showDialog) {
            this.$emit('md-opened')
          } else {
            this.$emit('md-closed')
          }
        } else {
          this.$refs.input.$el.click()
        }
      },
      onFocus () {
        if (this.mdOpenOnFocus) {
          this.toggleDialog(true)
        }
      },
      onFocusOut (e) {
        if (this.mdCloseOnBlur && this.$refs.mdRef.$el !== e.relatedTarget) {
          this.toggleDialog(false)
        }
      },
      inputDateToLocalDate () {
        if (this.inputDate) {
          if (this.parsedInputDate) {
            this.localDate = this.parsedInputDate
          }
        } else {
          this.localDate = null
        }
      },
      valueDateToLocalDate () {
        if (this.isModelNull) {
          this.localDate = null
        } else if (this.isModelTypeNumber) {
          this.localDate = new Date(this.value)
        } else if (this.isModelTypeDate) {
          this.localDate = this.value
        } else if (this.isModelTypeString) {
          let parsedDate = parse(this.value, this.dateFormat, new Date())

          if (isValid(parsedDate)) {
            this.localDate = parse(this.value, this.dateFormat, new Date())
          } else {
            Vue.util.warn(`The datepicker value is not a valid date. Given value: ${this.value}, format: ${this.dateFormat}`)
          }
        } else {
          Vue.util.warn(`The datepicker value is not a valid date. Given value: ${this.value}`)
        }
      },
      onClear () {
        this.$emit('md-clear')
      }
    },
    created () {
      this.inputDateToLocalDate = MdDebounce(this.inputDateToLocalDate, this.MdDebounce)
    }
  }
</script>

<style lang="scss">
  @import "~components/MdAnimation/variables";
  @import "~components/MdLayout/mixins";

  .md-datepicker-overlay {
    opacity: 0;

    @include md-layout-xsmall {
      opacity: 1;
    }
  }

  .md-datepicker {
    &.md-native {
      label {
        top: 0 !important;
      }
    }

    .md-date-icon {
      cursor: pointer;
    }

    input[type=date]::-webkit-clear-button,
    input[type=date]::-webkit-inner-spin-button,
    input[type=date]::-webkit-calendar-picker-indicator {
      display: none;
    }
  }
</style>