Untitled

 avatar
unknown
javascript
2 years ago
13 kB
4
Indexable
<script>
export default {
  name: 'FDatetimePicker',
  inheritAttrs: false,
  customOptions: {}
}
</script>

<script setup>
import { ref, watch, computed, unref } from 'vue'
import {
  BFormGroup,
  BInputGroup,
  BFormInput,
  BInputGroupAppend,
  BFormInvalidFeedback
} from 'bootstrap-vue-3'
import { FSelect } from '../select'
import { getDateString } from '../../services/date.service'
import { generateId } from '../utils'

const props = defineProps({
  modelValue: {
    type: null
  },
  rules: {
    type: [String],
    default: null
  },
  mode: {
    type: String,
    default: 'datetime',
    validator: value => ['datetime', 'date', 'time', 'month'].includes(value)
  },
  dateConfig: {
    type: Object,
    default: () => ({})
  },
  timeConfig: {
    type: Object,
    default: () => ({})
  },
  monthConfig: {
    type: Object,
    default: () => ({})
  },
  label: {
    type: String,
    default: ''
  },
  editable: {
    type: Boolean,
    default: false
  },
  disabled: {
    type: Boolean,
    default: false
  }
})

const emit = defineEmits(['update:modelValue'])

const { date, isValidDate, onClickDateInputClearIcon } = useDate()

const { time, isValidTime, timeSelectList, onClickTimeInputClearIcon } =
  useTime()

const { month, minMonth, maxMonth, getMonthForInputTag } = useMonth()

const { isVisibleDateInput, isVisibleTimeInput, isVisibleClearIcon } =
  useTemplateConditions()

const validationRules = computed(() => {
  const rules = []

  if (props.rules) {
    rules.push(unref(props.rules))
  }

  if (props.dateConfig.min || props.dateConfig.max) {
    const min =
      `${props.dateConfig.min} 00:00:00:000` || '1900-01-01 00:00:00:000'
    const max =
      `${props.dateConfig.max} 23:59:59:999` || '2100-12-31 23:59:59:999'

    rules.push(`date_between:${min},${max}`)
  }

  if (props.dateConfig.weekdaysOnly) {
    rules.push('date_weekdays')
  }

  return rules.join('|')
})

useModelValueWatchers()

function useDate() {
  const date = ref(null)

  const isValidDate = date => {
    const dateValue = new Date(date)

    return dateValue !== 'Invalid Date' && !isNaN(dateValue)
  }

  const onClickDateInputClearIcon = () => {
    date.value = null
  }

  return { date, isValidDate, onClickDateInputClearIcon }
}

function useTime() {
  const time = ref(null)

  const timeSelectList = computed(() => {
    if (props.timeConfig.intervals) {
      return getTimeSelectListWithInterval()
    } else {
      return getTimeSelectList()
    }
  })

  const isValidTime = time => {
    return /^([0-1]?[0-9]|2[0-4]):([0-5][0-9])(:[0-5][0-9])?$/.test(time)
  }

  const onClickTimeInputClearIcon = () => {
    time.value = null
  }

  const getTimeList = ({
    minHour,
    maxHour,
    minMinute,
    maxMinute,
    stepHours,
    stepMinutes,
    onlyStepUpHours
  }) => {
    let date = new Date()
    let maxDate = new Date()

    date.setHours(minHour)
    date.setMinutes(minMinute)

    maxDate.setHours(maxHour)
    maxDate.setMinutes(maxMinute)

    const timeList = []

    while (date <= maxDate) {
      const hour = date.getHours()
      const minute = date.getMinutes()

      timeList.push(
        `${hour.toString().padStart(2, '0')}:${minute
          .toString()
          .padStart(2, '0')}`
      )

      if (onlyStepUpHours) {
        date.setHours(date.getHours() + stepHours)
      } else {
        date.setMinutes(date.getMinutes() + stepMinutes)
      }
    }

    return timeList
  }

  const getTimeSelectList = () => {
    const stepHours = props.timeConfig.stepHours || 1
    const stepMinutes = props.timeConfig.stepMinutes || 15

    let minHour = 0
    let maxHour = 23
    let minMinute = 0
    let maxMinute = 59

    if (props.timeConfig.min) {
      const timeParts = props.timeConfig.min.split(':')

      minHour = parseInt(timeParts[0])
      minMinute = parseInt(timeParts[1])
    }

    if (props.timeConfig.max) {
      const timeParts = props.timeConfig.max.split(':')

      maxHour = parseInt(timeParts[0])
      maxMinute = parseInt(timeParts[1])
    }

    const onlyStepUpHours = props.timeConfig.stepHours ? true : false

    const timeList = getTimeList({
      minHour,
      minMinute,
      maxHour,
      maxMinute,
      stepHours,
      stepMinutes,
      onlyStepUpHours
    })

    return timeList
  }

  const getTimeSelectListWithInterval = () => {
    let parentTimeList = []

    for (const interval of props.timeConfig.intervals) {
      let minHour, minMinute, maxHour, maxMinute

      minHour = parseInt(interval.min.split(':')[0])
      minMinute = parseInt(interval.min.split(':')[1])
      maxHour = parseInt(interval.max.split(':')[0])
      maxMinute = parseInt(interval.max.split(':')[1])

      const stepHours = interval.stepHours || 1
      const stepMinutes = interval.stepMinutes || 15

      const onlyStepUpHours = interval.stepHours ? true : false

      const timeList = getTimeList({
        minHour,
        minMinute,
        maxHour,
        maxMinute,
        stepHours,
        stepMinutes,
        onlyStepUpHours
      })

      parentTimeList = [...parentTimeList, ...timeList]
    }

    return parentTimeList
  }

  return {
    time,
    isValidTime,
    timeSelectList,
    onClickTimeInputClearIcon
  }
}

function useMonth() {
  const month = ref(null)

  const minMonth = computed(
    () => getMonthForInputTag(props.monthConfig.min) || '1900-01'
  )

  const maxMonth = computed(
    () => getMonthForInputTag(props.monthConfig.max) || '2100-12'
  )

  const isValidMonth = month => {
    return (
      /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.test(month) ||
      /^([0-9]{4})-([0-9]{2})$/.test(month)
    )
  }

  const getMonthForInputTag = month => {
    if (isValidMonth(month) && month.length >= 7) {
      return month.slice(0, 7)
    }

    return month
  }

  return { month, isValidMonth, minMonth, maxMonth, getMonthForInputTag }
}

function useModelValueWatchers() {
  if (props.mode === 'datetime') {
    watch(
      () => props.modelValue,
      newValue => {
        if (!newValue) return
        const datetimeString = getDateString(new Date(newValue), {
          time: true,
          timezone: true
        })
        const datetimeParts = datetimeString.split('T')
        const timeParts = datetimeParts[1].split(':')

        date.value = datetimeParts[0]
        time.value = `${timeParts[0]}:${timeParts[1]}`
      },
      { immediate: true }
    )

    watch(
      () => date.value,
      newValue => {
        let value = null

        if (!time.value) {
          const { defaultValue } = props.timeConfig

          time.value = defaultValue
        }

        if (
          newValue &&
          time.value &&
          isValidDate(newValue) &&
          isValidTime(time.value)
        ) {
          value = getDateString(new Date(`${newValue} ${time.value}`), {
            time: true,
            timezone: true
          })
          if (!isValidDate(value)) value = null
        }

        emit('update:modelValue', value)
      }
    )

    watch(
      () => time.value,
      newValue => {
        let value = null

        if (
          newValue &&
          date.value &&
          isValidTime(newValue) &&
          isValidDate(date.value)
        ) {
          value = getDateString(new Date(`${date.value} ${newValue}`), {
            time: true,
            timezone: true
          })
          if (!isValidDate(value)) value = null
        }

        emit('update:modelValue', value)
      }
    )
  } else if (props.mode === 'date') {
    watch(
      () => props.modelValue,
      newValue => {
        date.value = newValue
      },
      { immediate: true }
    )

    watch(
      () => date.value,
      newValue => {
        const { startOfDay, endOfDay } = props.dateConfig
        let value = null

        if (newValue && isValidDate(newValue)) {
          if (startOfDay) {
            value = getDateString(new Date(`${newValue} 00:00:00:000`), {
              time: true,
              timezone: true
            })
          } else if (endOfDay) {
            value = getDateString(new Date(`${newValue} 23:59:59:999`), {
              time: true,
              timezone: true
            })
          } else {
            value = newValue
          }
        }

        emit('update:modelValue', value)
      }
    )
  } else if (props.mode === 'time') {
    watch(
      () => props.modelValue,
      newValue => {
        time.value = newValue
      },
      { immediate: true }
    )

    watch(
      () => time.value,
      newValue => {
        let value = null

        if (newValue && isValidTime(newValue)) {
          value = newValue
        }

        emit('update:modelValue', value)
      }
    )
  } else if (props.mode === 'month') {
    watch(
      () => props.modelValue,
      newValue => {
        month.value = getMonthForInputTag(newValue)
      },
      { immediate: true }
    )

    watch(
      () => month.value,
      newValue => {
        let value = null

        value = getMonthForInputTag(newValue)

        emit('update:modelValue', value)
      }
    )
  }
}

function useTemplateConditions() {
  const isVisibleDateInput = computed(
    () => props.mode === 'datetime' || props.mode === 'date'
  )

  const isVisibleTimeInput = computed(
    () =>
      (props.mode === 'datetime' || props.mode === 'time') &&
      !props.timeConfig.select
  )

  const isVisibleClearIcon = computed(() => {
    return !props.disabled && !props.editable
  })

  return {
    isVisibleDateInput,
    isVisibleTimeInput,
    isVisibleClearIcon
  }
}
</script>

<template>
  <ValidationField
    v-slot="{ errorMessage }"
    as="div"
    class="f-datetime-picker"
    :model-value="props.modelValue"
    :name="generateId()"
    :rules="validationRules"
    :validate-on-blur="false"
    :validate-on-change="false"
    :validate-on-input="false"
    :validate-on-model-update="true"
  >
    <b-form-group :label="label">
      <b-input-group>
        <div
          v-if="isVisibleDateInput"
          class="input-area"
          :class="{
            'date-input': props.mode === 'datetime',
            fixed: props.dateConfig.fixed
          }"
        >
          <b-form-input
            v-bind="dateConfig"
            v-model="date"
            ref="dateInputRef"
            :class="{
              'editable-disabled': !props.editable,
              'is-invalid': errorMessage
            }"
            :disabled="props.disabled"
            :min="props.dateConfig.min || '1900-01-01'"
            :max="props.dateConfig.max || '2100-12-31'"
            :name="generateId()"
            type="date"
          />
          <f-icon
            v-if="isVisibleClearIcon && date"
            bi
            class="clear-input"
            icon="x"
            @click="onClickDateInputClearIcon"
          />
        </div>
        <div
          v-if="isVisibleTimeInput"
          class="input-area time-input"
          :class="{
            fixed: props.mode === 'datetime' || props.timeConfig.fixed
          }"
        >
          <b-form-input
            v-bind="timeConfig"
            v-model="time"
            ref="timeInputRef"
            :class="{
              'editable-disabled': !props.editable
            }"
            :disabled="props.disabled"
            :name="generateId()"
            :placeholder="props.timeConfig.placeholder || 'HH:mm'"
            type="time"
          />
          <f-icon
            v-if="isVisibleClearIcon && time"
            bi
            class="clear-input"
            icon="x"
            @click="onClickTimeInputClearIcon"
          />
        </div>

        <input
          v-if="props.mode === 'month'"
          v-model="month"
          class="form-control"
          :class="{ 'is-invalid': errorMessage }"
          :disabled="props.disabled"
          :name="generateId()"
          type="month"
          :min="minMonth"
          :max="maxMonth"
        />

        <f-select
          v-if="props.mode === 'time' && props.timeConfig.select"
          v-model="time"
          class="time-select"
          :class="{
            fixed: props.timeConfig.fixed
          }"
          :disabled="props.disabled"
          :name="generateId()"
          :options="timeSelectList"
          :placeholder="props.timeConfig.placeholder || 'HH:mm'"
          value-type="string"
          :rules="rules"
          :is-error-message-visible="false"
        />
        <b-input-group-append
          v-if="props.mode === 'datetime' && props.timeConfig.select"
          class="input-append"
        >
          <f-select
            v-model="time"
            :rules="rules"
            class="time-select fixed"
            :disabled="props.disabled"
            :name="generateId()"
            :options="timeSelectList"
            :placeholder="props.timeConfig.placeholder || 'HH:mm'"
            value-type="string"
            :is-error-message-visible="false"
          />
        </b-input-group-append>

        <b-form-invalid-feedback
          id="inputLiveFeedback"
          :force-show="!!errorMessage"
        >
          {{ errorMessage }}
        </b-form-invalid-feedback>
      </b-input-group>
    </b-form-group>
  </ValidationField>
</template>
Editor is loading...