calendar.html
html
<!doctype html>
<html>
<head></head>
<body>
<div id="calendar"></div>
<script>
const range = (length) => Array.from({ length }, (_, i) => i);
const chunk = (arr, size) => {
if (size === 0) return arr;
return range(Math.ceil(arr.length / size)).map((i) =>
arr.slice(i * size, i * size + size)
);
};
const produceDate = (base, recipe) => {
const clonedDate = new Date(base);
recipe(clonedDate);
return clonedDate
};
const clearTime = (date) => {
return produceDate(date, (draft) => {
draft.setHours(0, 0, 0, 0)
})
};
const setDate = (date, day) => {
return produceDate(date, (draft) => {
draft.setDate(day);
})
};
const addMonth = (date, count) => {
return produceDate(date, (draft) => {
draft.setMonth(draft.getMonth() + count);
})
};
const subtractMonth = (date, count) => {
return addMonth(date, -count)
};
const toLastDay = (date) => {
return setDate(addMonth(date, 1), 0).getDate();
};
const toMonthDays = (date) => {
const WEEK_LENGTH = 7;
const firstDate = setDate(date, 1);
const lastDate = toLastDay(date);
const firstDateIndex = firstDate.getDay();
const restDay = (firstDateIndex + lastDate) % WEEK_LENGTH;
const nextMonthLength = restDay ? WEEK_LENGTH - restDay : restDay;
const prevMonthDays = range(firstDateIndex).fill('');
const currentMonthDays = range(lastDate).map((i) => setDate(date, i + 1)); // [1, 2, ...]
const nextMonthDays = range(nextMonthLength).fill('');
return chunk(
[
prevMonthDays,
currentMonthDays,
nextMonthDays
].flat(),
WEEK_LENGTH
)
};
const props = {
current: new Date()
};
const state = {
current: clearTime(props.current),
get monthDays() {
return toMonthDays(state.current)
},
get title() {
const current = state.current;
const year = current.getFullYear();
const month = current.getMonth() + 1;
return `${year}.${month}`
}
};
const mutation = {
mutateCurrent: (current) => {
state.current = current;
}
};
const actions = {
fetchNextMonth: () => {
const nextMonth = addMonth(state.current, 1);
mutation.mutateCurrent(nextMonth)
},
fetchPrevMonth: () => {
const prevMonth = subtractMonth(state.current, 1);
mutation.mutateCurrent(prevMonth)
}
}
</script>
<script>
const onClickPrev = () => {
actions.fetchPrevMonth();
render();
};
const onClickNext = () => {
actions.fetchNextMonth();
render();
};
const buttonTemplate = () => {
return `<div>
<button onclick="onClickPrev()" type="button">Prev</button>
<button onclick="onClickNext()" type="button">Next</button>
</div>`
};
const calendarTemplate = () => {
const weeks = state.monthDays
.map((week) => {
const days = week
.map((day) => {
return `<td>${day ? day.getDate() : ''}</td>`
})
.join('');
return `<tr>${days}</tr>`
})
.join('');
return `<table>${weeks}</table>`
};
const render = () => {
const template = `
${buttonTemplate()}
<div>${state.title}</div>
${calendarTemplate()}
`;
document.querySelector('#calendar').innerHTML = template
};
render();
</script>
</body>
</html>
common-calendar.vue
vue
<template>
<div>
<div>{{ title }}</div>
<div>
<button @click="onClickPrev" type="button">Prev</button>
<button @click="onClickNext" type="button">Next</button>
</div>
<div v-for="(week, weekIndex) of monthDays" :key="weekIndex">
<span
v-for="(day, dayIndex) of week"
:key="dayIndex"
:style="{ fontWeight: isSelectedData(day) ? 'bold' : '' }"
@click="onClickDay(day)"
>
{{ day | toDayTitle }}
</span>
</div>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
reactive,
toRefs
} from '@vue/composition-api'
interface CalendarProps {
value: Date
}
interface CalendarOption {
year: number
month: number
day: number
}
type LastDay = 28 | 29 | 30 | 31
type DayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6
const cloneDate = (date: Date): Date => {
return new Date(date.getTime())
}
const addMonth = (date: Date, count: number): Date => {
const clonedDate = cloneDate(date)
clonedDate.setMonth(clonedDate.getMonth() + count)
return clonedDate
}
const toNextMonth = (date: Date): Date => {
return addMonth(date, 1)
}
const toPrevMonth = (date: Date): Date => {
return addMonth(date, -1)
}
const setDay = (date: Date, day: number): Date => {
const clonedDate = cloneDate(date)
clonedDate.setDate(day)
return clonedDate
}
const toDayIndex = (date: Date): DayIndex => date.getDay() as DayIndex
const toCalendarOption = (date: Date): CalendarOption => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
return { year, month, day }
}
const isLeapMonth = (date: Date): boolean => {
const LEAP_YEAR = 2000
const LEAP_YEAR_PERIOD = 4
const MONTH_FEBRUARY = 2
const { year, month } = toCalendarOption(date)
const diffYear = Math.abs(year - LEAP_YEAR)
return diffYear % LEAP_YEAR_PERIOD === 0 && month === MONTH_FEBRUARY
}
const toLastDay = (date: Date): LastDay => {
const { month } = toCalendarOption(date)
const LAST_DAYS: LastDay[] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
const LEAP_YEAR_LAST_DATE: LastDay = 29
return isLeapMonth(date) ? LEAP_YEAR_LAST_DATE : LAST_DAYS[month - 1]
}
const range = (length: number): number[] => Array.from({ length }, (_, i) => i)
function chunk<T>(arr: T[], size: number): T[][] {
const length = Math.ceil(arr.length / size)
const newArr = Array.from({ length }, () => [])
arr.forEach((value: number, index) => {
const newArrIndex = Math.floor(index / size)
newArr[newArrIndex].push(value)
})
return newArr
}
const toMonthDays = (date: Date): Array<Date | string>[] => {
const WEEK_LENGTH = 7
const firstDay = setDay(date, 1)
const dayIndex = toDayIndex(firstDay)
const lastDate = toLastDay(date)
const restDay = (dayIndex + lastDate) % WEEK_LENGTH
const nextMonthLength = restDay ? WEEK_LENGTH - restDay : restDay
const prevMonthDays = range(dayIndex).map(() => '')
const currentMonthDays = range(lastDate).map((_, i) => setDay(date, i + 1)) // [1, 2, ...]
const nextMonthDays = range(nextMonthLength).map(() => '')
return chunk(
[].concat(prevMonthDays, currentMonthDays, nextMonthDays),
WEEK_LENGTH
)
}
const clearTime = (date: Date): Date => {
const clonedDate = cloneDate(date)
clonedDate.setHours(0, 0, 0, 0)
return clonedDate
}
export default defineComponent({
props: {
value: Date
},
filters: {
toDayTitle: (date: Date | null): string => {
return date ? String(date.getDate()) : '[ ]'
}
},
setup(props: CalendarProps, context) {
const state = reactive({
current: props.value,
monthDays: computed(() => toMonthDays(state.current)),
title: computed(() => {
const { year, month } = toCalendarOption(state.current)
return `${year}/${month}`
})
})
const onClickNext = () => {
state.current = toNextMonth(state.current)
}
const onClickPrev = () => {
state.current = toPrevMonth(state.current)
}
const onClickDay = (date: Date) => {
state.current = cloneDate(date)
context.emit('change', cloneDate(date))
}
const isSelectedData = (date: Date): boolean => {
if (!date) {
return false
}
return +clearTime(date) === +clearTime(state.current)
}
return {
...toRefs(state),
onClickNext,
onClickPrev,
onClickDay,
isSelectedData
}
}
})
</script>
to-month-days.js
js
const {toDayIndex, toLastDay} = require('../../Javascript/Date/fp')
const {chunk, range} = require('../../Functional/fp')
const toMonthDays = ({year, month}) => {
const WEEK_LENGTH = 7
const date = new Date(year, month - 1, 1)
const dayIndex = toDayIndex(date)
const lastDate = toLastDay({year, month})
const restDay = (dayIndex + lastDate) % WEEK_LENGTH
const nextMonthLength = restDay ? WEEK_LENGTH - restDay : restDay
const prevMonthDays = range(dayIndex).map(() => '')
const currentMonthDays = range(lastDate).map((_, i) => i + 1) // [1, 2, ...]
const nextMonthDays = range(nextMonthLength).map(() => '')
return chunk([].concat(prevMonthDays, currentMonthDays, nextMonthDays), WEEK_LENGTH)
}
module.exports = toMonthDays
to-next-month.js
js
const toNextMonth = (option) => {
const FIRST_MONTH = 1
const LAST_MONTH = 12
const nextMonth = option.month + 1
const isNextYear = nextMonth > LAST_MONTH
const month = isNextYear ? FIRST_MONTH : nextMonth
const year = option.year + (isNextYear ? 1 : 0)
return { year, month }
}
module.exports = toNextMonth
to-prev-month.js
js
const toPrevMonth = (option) => {
const FIRST_MONTH = 1
const LAST_MONTH = 12
const prevMonth = option.month - 1
const isPrevYear = prevMonth < FIRST_MONTH
const month = isPrevYear ? LAST_MONTH : prevMonth
const year = option.year - (isPrevYear ? 1 : 0)
return { year, month }
}
module.exports = toPrevMonth