isuke
2/28/2017 - 3:21 PM

SmartCalendar.vue

<template lang="pug">
.smart-datepicker
  smart-calendar(
    :$_calendarable_default-year="$_calendarable_defaultYear",
    :$_calendarable_default-month="$_calendarable_defaultMonth",
    :$_calendarable_default-selected-dates="$_calendarable_defaultSelectedDates",
    :$_calendarable_start-day-of-week="$_calendarable_startDayOfWeek",
    :$_calendarable_select-mode="$_calendarable_selectMode",
    :$_calendarable_disable-dates="$_calendarable_disableDates",
    :$_calendarable_classes="$_calendarable_classes",
    :$_calendarable_week-names="$_calendarable_weekNames",
    @update:date="selectDate")
  input.input(type="text", :value="dateStr")
</template>

<script lang="coffee">
import SmartCalendar from '@components/SmartCalendar.vue'
import calendarable from '@scripts/mixins/calendarable.coffee'

export default
  name: 'SmartDatepicker'
  introduction: "TODO"
  token: """
    <smart-datepicker>
    </smart-datepicker>
  """
  description: """
    <p>TODO</p>
  """
  mixins: [calendarable]
  data: ->
    dates: []
  computed:
    dateStr: ->
      _(@dates)
        .map (date) =>
          "#{date.year}/#{date.month}/#{date.day}"
        .join(', ')
  methods:
    selectDate: (value) ->
      @dates = value
  components:
    'smart-calendar': SmartCalendar
</script>

<style lang="stylus" scoped>
.smart-datepicker {}
</style>
export default
  props:
    $_calendarable_defaultYear:
      type: Number
      require: false
      default: -> moment().year()
      note: "current display year. default today's year."
    $_calendarable_defaultMonth:
      type: Number
      require: false
      default: -> moment().month() + 1
      note: "current display month. default today's month."
    $_calendarable_defaultSelectedDates:
      type: Array
      require: false
      default: -> [moment().format('YYYY-MM-DD')]
      note: "default initial selected date. ex) ['2018-01-15']. default today."
    $_calendarable_startDayOfWeek:
      type: Number
      require: false
      default: 1
      note: "start day of week. 0: Sun, 1: Mon, .., 6: sat"
    $_calendarable_selectMode:
      type: String
      require: false
      default: 'single'
      validator: (val) -> ['single', 'multiple', 'range'].includes(val)
      node: "select mode. 'single', 'multiple' or 'range'."
    $_calendarable_disableDates:
      type: Array
      require: false
      default: -> []
      # default: -> # DEBUG
      #   [
      #     (momentValue) -> (momentValue.day() == 3 || momentValue.day() == 4)
      #   ]
      note: """
        disable any date.
        ex)
        [
          function(momentValue) {
            return (momentValue.day() === 0 || momentValue.day() === 6);
          },
          function(date) {
            return momentValue.isBefore('2018-03-15');
          },
        ]
      """
    $_calendarable_classes:
      type: Object
      require: false
      dafault: -> []
      # default: -> # DEBUG
      #   {
      #     '-special': (momentValue) ->
      #       range = moment.range(moment('2018-3-5'), moment('2018-3-13'))
      #       momentValue.within(range)
      #   }
      note: """
        Attach any (HTML) classes by date.
        ex)
        {
          'special-date': function(momentValue) {
            const range = moment.range(moment('2018-3-5'), moment('2018-3-13'))
            return momentValue.within(range)
          }
        });
      """
    $_calendarable_weekNames:
      type: Array
      require: true
      default: -> ['日', '月', '火', '水', '木', '金', '土']
      validator: (val) -> val.length == 7
      note: "day of week names. must start from Sunday. ex) ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']"
  computed:
    $_calendarable_isSingleMode:   -> @$_calendarable_selectMode == 'single'
    $_calendarable_isMultipleMode: -> @$_calendarable_selectMode == 'multiple'
    $_calendarable_isRangeMode:    -> @$_calendarable_selectMode == 'range'
<template lang="pug">
.smart-calendar
  .buttons
    button.prev(@click="subtractMonth") ◀
    button.next(@click="addMonth") ▶
  p.title {{currentYear}}年 {{currentMonth}}月
  table.table
    thead.head
      tr.row
        td.col(v-for="dayOfWeek in weekIndexes", :class="headClass(dayOfWeek)") {{$_calendarable_weekNames[dayOfWeek]}}
    tbody.body
      tr.row(v-for="week in calendar")
        td.col(
          v-for="date in week",
          :class="dateClass(date)",
          @click="selecteDate(date)"
          @mouseover="hoverDate(date)") {{date.day}}
</template>

<script lang="coffee">
import calendarable from '@scripts/mixins/calendarable.coffee'

export default
  name: 'SmartCalendar'
  introduction: "TODO"
  token: """
    <smart-calendar>
    </smart-calendar>
  """
  description: """
    <p>TODO</p>
  """
  mixins: [calendarable]
  data: ->
    weekForClassName: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
    currentYear: undefined
    currentMonth: undefined
    selectedDates: []
    isSelectingRange: false
    hoverStartDate:
      year: undefined
      month: undefined
      date: undefined
      dayOfWeek: undefined
    hoverdDate:
      year: undefined
      month: undefined
      date: undefined
      dayOfWeek: undefined
  computed:
    calendar: ->
      currentMonthMoment = @createMoment()

      prevMonthMoment = currentMonthMoment.clone().subtract(1, 'month')
      nextMonthMoment = currentMonthMoment.clone().add(1, 'month')

      prevMonthLastDay    = prevMonthMoment.daysInMonth()
      currentMonthLastDay = currentMonthMoment.daysInMonth()

      currentMonthFirstDayOfWeek = @weekIndexes.indexOf(currentMonthMoment.day())
      nextMonthFirstDayOfWeek    = @weekIndexes.indexOf(nextMonthMoment.day())

      prevMonthYear    = prevMonthMoment.year()
      currentMonthYear = currentMonthMoment.year()
      nextMonthYear    = nextMonthMoment.year()

      prevMonth    = prevMonthMoment.month() + 1
      currentMonth = currentMonthMoment.month() + 1
      nextMonth    = nextMonthMoment.month() + 1

      calendar = []
      dayIdx = 1
      for w in [0..5]
        week = []
        break if currentMonthLastDay < dayIdx
        for d in [0..6]
          if w == 0 and d < currentMonthFirstDayOfWeek
            week[d] =
              year: prevMonthYear
              month: prevMonth
              day: prevMonthLastDay - (currentMonthFirstDayOfWeek - d) + 1
              dayOfWeek: @weekIndexes[d]
          else if currentMonthLastDay < dayIdx
            week[d] =
              year: nextMonthYear
              month: nextMonth
              day: d - nextMonthFirstDayOfWeek + 1
              dayOfWeek: @weekIndexes[d]
            dayIdx++
          else
            week[d] =
              year: currentMonthYear
              month: currentMonth
              day: dayIdx
              dayOfWeek: @weekIndexes[d]
            dayIdx++
        calendar.push(week)
      calendar
    weekIndexes: ->
      w = [0..6]
      _.times @$_calendarable_startDayOfWeek, (_n) =>
        tail = _.tail(w)
        tail.push(_.head(w))
        w = tail
      w
  methods:
    createMoment: (year = @currentYear, month = @currentMonth, day = 1) -> moment({ year: year, month: month - 1, day: day})
    createMomentByDate: (date) -> @createMoment(date.year, date.month, date.day)
    createMomentRangeByDates: (date1, date2) ->
      moment1 = @createMomentByDate(date1)
      moment2 = @createMomentByDate(date2)
      if moment1.isBefore(moment2)
        moment.range(moment1, moment2)
      else
        moment.range(moment2, moment1)
    toDateFromMoment: (moment) ->
      {
        year:      moment.year()
        month:     moment.month() + 1
        day:       moment.date()
        dayOfWeek: moment.day()
      }
    toDatesFromMomentRaange: (momentRange, ignoreDisable = true) ->
      _(Array.from(momentRange.by('days')))
        .map (moment) =>
          @toDateFromMoment(moment) if ignoreDisable && ! @isDisable(moment)
        .compact()
        .value()
    addMonth: ->
      nextMonthMoment = @createMoment().add(1, 'month')
      @currentYear  = nextMonthMoment.year()
      @currentMonth = nextMonthMoment.month() + 1
      @$emit('update:month', @currentYear, @currentMonth)
    subtractMonth: ->
      prevMonthMoment = @createMoment().subtract(1, 'month')
      @currentYear  = prevMonthMoment.year()
      @currentMonth = prevMonthMoment.month() + 1
      @$emit('update:month', @currentYear, @currentMonth)
    selecteDate: (date) ->
      moment = @createMomentByDate(date)
      unless @isDisable(moment)
        switch @$_calendarable_selectMode
          when 'single'
            @selectedDates = [date]
          when 'multiple'
            if @includesDate(@selectedDates, date)
              @selectedDates = _.filter @selectedDates, (selectedDate) => ! @isEqDate(selectedDate, date)
            else
              @selectedDates.push(date)
          when 'range'
            if @isSelectingRange
              @isSelectingRange = false
              hoverEndDate = @hoverdDate || @hoverStartDate
              rangeMoment = @createMomentRangeByDates(@hoverStartDate, hoverEndDate)
              @selectedDates = @toDatesFromMomentRaange(rangeMoment)
              @hoverStartDate = undefined
              @hoverdDate = undefined
            else
              @selectedDates = []
              @hoverStartDate = date
              @isSelectingRange = true
          else
            throw "error"
        @$emit('update:date', @selectedDates)
    hoverDate: (date) ->
      if @$_calendarable_isRangeMode && @isSelectingRange
        @hoverdDate = date
    headClass: (dayOfWeek) ->
      {
        "-#{@weekForClassName[dayOfWeek]}": true
      }
    dateClass: (date) ->
      moment = @createMomentByDate(date)
      if @$_calendarable_isRangeMode && @isSelectingRange && @hoverStartDate && @hoverdDate
        momentRange = @createMomentRangeByDates(@hoverStartDate, @hoverdDate)
      result = {
        '-selected': @includesDate(@selectedDates, date)
        "-#{@weekForClassName[date.dayOfWeek]}": true
        "-disable": @isDisable(moment)
        "-inhover": momentRange && moment.within(momentRange)
        "-hoverstart": momentRange && momentRange.start.isSame(moment)
        "-hoverend": momentRange && momentRange.end.isSame(moment)
      }
      _.forEach @$_calendarable_classes, (func, className) => result[className] = true if func(moment)

      result
    isEqDate: (date1, date2) ->
      date1.year == date2.year &&
        date1.month == date2.month &&
        date1.day == date2.day
    includesDate: (dates, date) ->
      _.reduce dates, (result, d) =>
        result || @isEqDate(d, date)
      , false
    isDisable: (moment) ->
      _.reduce @$_calendarable_disableDates, (result, func) =>
        result || func(moment)
      , false
  created: ->
    @currentYear  = @$_calendarable_defaultYear
    @currentMonth = @$_calendarable_defaultMonth

    _.forEach @$_calendarable_defaultSelectedDates, (defaultSelectedDate) =>
      m = moment(defaultSelectedDate, 'YYYY-MM-DD')
      d = @toDateFromMoment(m)
      @selectedDates.push(d)

  mounted: -> null
</script>

<style lang="stylus" scoped>
.smart-calendar {
  background-color: white; /* DEBUG */
  color: black; /* DEBUG */
  > .table {
    > .head, > .body {
      .row {
        .col {
          border: 1px solid black; /* DEBUG */
          &.-sun {
            background-color: #FF6666;  /* DEBUG */
          }
          &.-sat {
            background-color: #6666FF;  /* DEBUG */
          }
        }
      }
    }

    > .head {
      .row {
        .col {
        }
      }
    }

    > .body {
      .row {
        .col {
          cursor: pointer;

          &.-special { /* DEBUG */
            &:before { content: "★" }
          }
          &.-inhover {
            text-decoration: underline;
          }
          &.-hoverstart {
            &:before { content: "【" } /* DEBUG */
          }
          &.-hoverend {
            &:after { content: "】" } /* DEBUG */
          }
          &.-selected {
            color: red; /* DEBUG */
          }
          &.-disable {
            cursor: default;
            background-color: gray;  /* DEBUG */
          }
        }
      }
    }
  }
}
</style>