Vuetifyのdatepickerを使って【和暦】＋【年度/月】pickerを作ってみた


vuetifyのv-date-pickerは、表面上のテキストをfuncitionで書き換えることができるので【和暦表示】と【年度/月】が選択できるピッカーを作ってみました。
**0.5x倍**にしてみると普通に見れると思います。

<p class="codepen" data-height="419" data-theme-id="default" data-default-tab="js,result" data-user="bigfly3" data-slug-hash="mddNLjM" style="height: 419px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="WarekiDatePicker">
  <span>See the Pen <a href="https://codepen.io/bigfly3/pen/mddNLjM">
  WarekiDatePicker</a> by BigFly3 (<a href="https://codepen.io/bigfly3">@bigfly3</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

本来やらないような操作したときに稀にエラーになるかもしれませんが、公式のもおかしな感じになるのでそこは無視してください。

## 親からも和暦情報が取れます
ピッカーだけ和暦になっても、コンポーネントの連携での恩恵が薄いので、@changeの際にオブジェクトで和暦情報を送るようにしています。

### データについて
v-modelでセットされるデータは普通のピッカーと同様（YYYY-MM-DD）です.
和暦のオブジェクトに関してはcodepenの日付を選択してもらうと、typeによって取れる情報が確認できます。年度に関しては、リアル年と年度年の両方取得できます。

```js
@change　→　和暦のデータを含めたオブジェクトの取得
@input　→　valueの取得
```

### フォーマット変更したい場合
下にあるcomponentのjpv～Format()のメソッド部分をいじれば色々変えられるかと思います。

### オブジェクトで返すデータを調整したい場合
下にあるcomponentのjpvDate()の中で調整すると、自分好みのデータを返すことが出来ると思います。


## component化したもの

v-date-pickerをラッパー化して、フォーマット関連以外のプロパティは、ほぼそのまま使えるようにしてあります。v-date-pickerから置き換えてもそのまま動くと思います。

あんまり自分で使ったわけではないですが、vuetifyの公式サンプルが動くくらいにはなってると思います。よかったらお試しください。


```js:jpv-date-picker
<template>
<v-date-picker
  :allowedDates='allowedDates'
  :color='color'
  :dark='dark'
  :dayFormat='jpvDayFormat'
  :disabled='disabled'
  :eventColor='eventColor'
  :events='events'
  :firstDayOfWeek='firstDayOfWeek'
  :fullWidth='fullWidth'
  :headerColor='headerColor'
  :headerDateFormat='str => jpvHeaderDateFormat(str)'
  :landscape='landscape'
  :light='light'
  locale='ja-jp'
  :max='max'
  :min='min'
  :monthFormat='str => jpvMonthFormat(str)'
  :multiple='multiple'
  :nextIcon='nextIcon'
  :noTitle='noTitle'
  :pickerDate='pickerDate'
  :prevIcon='prevIcon'
  :range='range'
  :reactive='reactive'
  :readonly='readonly'
  :scrollable='scrollable'
  :selectedItemsText='selectedItemsText'
  :showCurrent='pickerShowCurrent'
  :showWeek='showWeek'
  :titleDateFormat=jpvTitleDateFormat
  :type='jpvType'
  :value='jpvValue'
  :width='width'
  :yearFormat='str => jpvYearFormat(str)'
  :yearIcon='yearIcon'
  @change="changeValue"
  @click:month='clickMonth'
  @click:date='clickDate'
  @update:picker-date='updatePickerDate'

  ><slot></slot></v-date-picker>
</template>
<script>
export default {
  props:{
    allowedDates: Function,
    color: String,
    dark: Boolean,
    dayFormat: Function,
    disabled: Boolean,
    eventColor: [Array, Function, Object, String],
    events: [Array, Function, Object],
    firstDayOfWeek: [String, Number],
    fullWidth: Boolean,
    headerColor: String,
    landscape: Boolean,
    light: Boolean,
    max: String,
    min: String,
    multiple: Boolean,
    nextIcon: String,
    noTitle: Boolean,
    pickerDate: String,
    prevIcon: String,
    range: Boolean,
    reactive: Boolean,
    readonly: Boolean,
    scrollable: Boolean,
    showCurrent: [Boolean, String],
    showWeek: [Boolean],
    selectedItemsText: String,
    type: {
      type: String,
      default: 'date' // 'date' or 'month' or 'nendo'
    },
    value:[String, Array, Object],
    width:[Number, String],
    yearIcon: String
  },
  data: () => {
    const now = new Date();
    return {
      inputValue:'',
      now:now,
    }
  },
  created(){
    this.inputValue = this.value
  },
  methods:{
    real2nendoM(realM){ // 内部/月 → 年度/月
      return realM < 10 ? realM + 3 : realM - 9
    },
    real2nendoY(realY,realM){ // 内部/月 → 年度/年
      return realM < 10 ? realY : realY + 1
    },
    real2nendoYM(realYM){ // 内部/年月 → 年度/年月
      if(!realYM) return ''
      const y = parseInt(realYM.split('-')[0]);
      const m = parseInt(realYM.split('-')[1]);
      return this.real2nendoY(y,m) + '-' + this.strpad(this.real2nendoM(m))
    },
    nendo2realM(nendoM){ // 年度/月 → 内部/月
      return nendoM < 4 ? nendoM + 9 : nendoM - 3
    },
    nendo2realY(nendoY,nendoM){ // 年度/年月 → 内部/年
      return nendoM < 4 ? nendoY - 1 : nendoY
    },
    nendo2realYM(nendoYM){ // 年度/年月 → 内部/年月
      if(!nendoYM) return ''
      const y = parseInt(nendoYM.split('-')[0]);
      const m = parseInt(nendoYM.split('-')[1]);
      return this.nendo2realY(y,m) + '-' + this.strpad(this.nendo2realM(m))
    },
    year2Wareki(year){
      let wYear = ''
      let gen = ''
      if(year > 2018){
        wYear = year-2018
        gen = '令和'
      }else if(year > 1988){
        wYear= year-1988
        gen = '平成'
      }else if(year > 1925){
        wYear = year-1925
        gen = '昭和'
      }else if(year > 1911){
        wYear = year-1911
        gen = '大正'
      }else if(year > 1867){
        wYear = year-1867
        gen = '明治'
      }
      if(wYear === 1) wYear = '元'
      return gen !== '' ? gen + wYear + '年' : false
    },
    strpad(num){
      return ( '00' + num ).slice( -2 );
    },
    changeValue:function(str){
      if(!str) return false
      if(this.readonly) return false
      if(this.type === 'nendo'){
        str = this.real2nendoYM(str)
      }
      this.inputValue = str
      this.$emit('change',this.jpvDate)
    },
    clickMonth:function(str){
      if(this.readonly) return false
      if(this.type === 'nendo'){
        str = this.real2nendoYM(str)
      }
      if(this.type !== 'date'){
        this.updateValue(str)
      }
      this.$emit('input',this.inputValue)
    },
    clickDate:function(str){
      if(this.readonly) return false
      this.updateValue(str)
      this.$emit('input',this.inputValue)
    },
    updateValue(str){
      if(this.multiple){
        const index = this.inputValue.indexOf(str)
        if(index > -1){
          this.inputValue = this.inputValue.filter(n => n != str);
        }else{
          this.inputValue.push(str)
        }
      }else if(this.range){
        if(this.inputValue.length > 1){
          this.inputValue = []
        }
        this.inputValue.push(str)
      }
    },
    updatePickerDate(str){
      this.$emit('update:picker-date',str)
    },

    //ヘッダーの一番上にある年表示＋年選択のフォーマット
    jpvYearFormat(str){
      if(!str) return ''
      const year = this.type==='date' ? str.split('-')[0]: str
      return this.year2Wareki(year) + '(' + year + ')'
    },
    //ヘッダーの選択データを表示する部分
    jpvTitleDateFormat(str){ // realYM → YYYY-MM
      if(!str) return ''
      return this.multiple || this.range ? String(this.inputValue.length) + ' selected' : this.inputValue
    },
    //月選択ボタン上部のYYYYを変換するfunction
    jpvHeaderDateFormat(str){
      if(!str) return ''
      const [year,month,day] = str.split('-')
      if(this.type === 'nendo'){
        return this.year2Wareki(year) + '度(' + year + ')'
      }else if(this.type === 'date' && month){ //dateの場合でも月選択するケースがある
        return this.year2Wareki(year)  + '(' + year + ')' + month + '月'
      }else{
        return this.year2Wareki(year) + '(' + year + ')'
      }
      return str
    },
    //月選択ボタンを年度の見た目にするfunction
    jpvMonthFormat(str){ // realYM → YYYY-MM
      if(str === '') return str
      let m = parseInt(str.split('-')[1])
      if(this.type === 'nendo') m = this.real2nendoM(m)
      return m + '月'
    },
    //月選択ボタンを年度の見た目にするfunction
    jpvDayFormat(str){ // realYMD → YYYY-MM-DD
      if(str === '') return str
      let d = parseInt(str.split('-')[2])
      return d
    },
  },
  computed:{
    pickerShowCurrent(){ //picker値は元のまま変わらないのでpickerの位置を3か月ずらす
      if(!this.showCurrent){ //カレント表示をなくす場合
        return false
      }
      let current = this.showCurrent
      if(this.type === 'nendo'){
        //初期状態でカレントがセットされていない場合、内部を現時刻から3か月前にセット
        if(current === true){
          current = this.now.getFullYear() + '-' +  parseInt(this.now.getMonth() + 1);
        }
        current = this.nendo2realYM(current)
      }
      return current
    },
    lastValue(){
      return this.multiple || this.range ? this.inputValue[this.inputValue.length - 1] : this.inputValue
    },
    jpvType(){
      return this.type==='nendo' ? 'month' : this.type
    },
    jpvDate(){
      if(typeof this.inputValue !== 'string' || !this.inputValue.match( /^(\d{4})/ )) return ""

      const d = {}
      d.value = this.inputValue
      let [year,month,day] = d.value.split('-')

      d.year = parseInt(year)
      d.month = month
      d.day = day ? day : ''
      d.wareki = this.year2Wareki(d.year)

      d.WM = d.wareki + d.month + '月'
      d.WYM = d.wareki + '(' + d.year + '/' + d.month + ')'

      //表示用のデータ
      if(this.type==='date'){
        d.value = d.year + '-' + d.month + '-' + d.day
        d.WMD = d.wareki + d.month + '月' + d.day + '日'
        d.WYMD = d.wareki + '(' + d.year + '/' + d.month + '/' + d.day + ')'
      }else if(this.type==='nendo'){
        d.nendo = {}
        d.nendo.year = d.month < 4 ? d.year -1 : d.year
        d.nendo.month = d.month
        d.nendo.wareki = this.year2Wareki(d.nendo.year) + '度'
        d.nendo.WM = d.nendo.wareki + d.nendo.month + '月'
        d.nendo.WYM = d.nendo.wareki + '(' + d.nendo.year + '/' + d.nendo.month + ')'
      }
      return d
    },
    jpvValue(){
      if(!this.inputValue){
        return this.multiple || this.range ? []:"";
      }
      let returnValue = this.inputValue;
      if(this.type==='nendo'){
        if(this.multiple || this.range){
          returnValue = this.inputValue.map((str)=>{
            return str.match( /^(\d{4})-(\d{2})$/ ) ? this.nendo2realYM(str) : str
          })
        }else{
          returnValue = this.nendo2realYM(this.inputValue)
        }
      }
      return returnValue
    },
  },
  watch:{
    multiple:{
      handler(newVal){
        this.inputValue = newVal ? []: ''
        this.$emit('input',this.inputValue)
      }
    },
    range:{
      handler(newVal){
        this.inputValue = newVal ? []: ''
        this.$emit('input',this.inputValue)
      }
    },
    type:{
      handler(newVal){ //typeを切り替えるとv-date-pickerとの整合性が合わないので再設定する
        this.$children[0].tableDate = this.$children[0].inputDate
      }
    },
  }
}
</script>
```

[:christmas_tree: FORK Advent Calendar 2019](/advent-calendar/2019/fork)
:arrow_left: 08日目 [Timelineをもう一度](/AsaToBan/items/bba1d6f47f780ada7d57) @AsaToBan
:arrow_right: 10日目 [Google Translation API v3 を Node で使ってみた](https://qiita.com/karaage7/items/9a96efe413878002ac13) @karaage7