All files / src/components/atoms RequiredDatePicker.vue

89.87% Statements 71/79
83.33% Branches 15/18
75% Functions 3/4
89.87% Lines 71/79

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139  1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   1x 1x   1x   1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x         1x 1x 1x           1x                             1x           1x 1x       1x   1x 5x 1x     1x 5x 5x   5x 3x 3x 5x     1x 1x   1x       1x 1x 1x         1x         1x 1x 1x 1x 1x     1x 1x         1x   1x 1x 1x         1x 1x 1x 1x      
<template>
  <div>
    <!-- 親要素を activator にする -->
    <BaseRequiredField
      is="v-text-field"
      :model-value="displayValue"
      @update:model-value="onInput"
      :label-key="labelKey"
      :label="label"
      :required="required"
      readonly
      v-bind="$attrs"
    >
      <template v-for="(_, name) in $slots" #[name]="slotProps">
        <slot :name="name" v-bind="slotProps"></slot>
      </template>
    </BaseRequiredField>
 
    <v-menu
      v-model="menu"
      activator="parent"
      :close-on-content-click="false"
      transition="scale-transition"
      offset-y
    >
      <v-locale-provider :locale="resolvedLocale">
        <v-date-picker
          :model-value="pickerDate"
          @update:model-value="onDateSelected"
          :min="min"
          :max="max"
          :title="pickerTitle"
        />
      </v-locale-provider>
    </v-menu>
  </div>
</template>
 
<script setup lang="ts">
// region import
import { computed, ref, watch } from 'vue'
import BaseRequiredField from './BaseRequiredField.vue'
import { getBrowserLocale, normalizeToDateString, parsePickerDate } from '@/utils'
// endregion import
 
defineOptions({ name: 'RequiredDatePicker' })
 
// region props
const props = withDefaults(defineProps<{
  modelValue?: string | null
  labelKey?: string
  label?: string
  required?: boolean
  min?: string
  max?: string
  locale?: string
  dateFormat?: 'yyyy-mm-dd' | 'yyyy-mm' | 'mm-dd'
}>(), {
  dateFormat: 'yyyy-mm-dd'
})
// endregion props
 
// region emits
const emit = defineEmits<{
  (e: 'update:modelValue', v: string | null): void
}>()
// endregion emits
 
// region state
const menu = ref(false)
const innerValue = ref<string | Date | null>(props.modelValue ?? null)
// endregion state
 
// region computed
const resolvedLocale = computed(() => props.locale?.trim() || getBrowserLocale('ja'))
 
const pickerDate = computed(() => {
  return parsePickerDate(innerValue.value)
})
// 表示用の値(とりあえずそのまま、必要ならここでフォーマット)
// const displayValue = computed(() => normalizeToDateString(innerValue.value) ?? '')
const displayValue = computed(() => {
  const normalized = normalizeToDateString(innerValue.value)
  if (!normalized) return ''
 
  if (props.dateFormat === 'yyyy-mm') {
    return normalized.substring(0, 7)
  }
  if (props.dateFormat === 'mm-dd') {
    return normalized.substring(5, 10)
  }
  return normalized
})
 
const pickerTitle = computed(() => props.label ?? '')
// endregion computed
 
// region watchers
watch(
  () => props.modelValue,
  (val) => {
    if (val !== innerValue.value) {
      innerValue.value = val ?? null
    }
  }
)
// endregion watchers
 
// region handlers
// テキストフィールド側を直接編集されたとき(基本 readonly だが一応ケア)
const onInput = (value: string) => {
  const normalized = normalizeToDateString(value)
  innerValue.value = normalized
  emit('update:modelValue', normalized)
}
 
// DatePicker で日付を選択したとき
const onDateSelected = (value: unknown) => {
  const normalized = normalizeToDateString(value)
  // フォーマット指定がある場合は、ここで整形して格納することも可能だが、
  // データとしては YYYY-MM-DD (または YYYY-MM-01) の完全な日付文字列を保持し、
  // 表示のみ切り替えるのが安全な場合が多い。
  // ただし、バックエンドが YYYY-MM を期待している場合はここで切る。
  let result = normalized
 
  if (normalized && props.dateFormat === 'yyyy-mm') {
    result = normalized.substring(0, 7)
  } else if (normalized && props.dateFormat === 'mm-dd') {
    // mm-dd だけ保存するのは稀だが、要件に合わせて実装
    result = normalized.substring(5, 10)
  }
 
  innerValue.value = result
  emit('update:modelValue', result)
  menu.value = false
}
// endregion handlers
</script>