Last active
July 2, 2024 12:16
-
-
Save edp1096/6dbadb0ff37303cb81de64407195bb0f to your computer and use it in GitHub Desktop.
Alpine.js를 이용한 간단한 달력. 데모: https://edp1096.github.io/hello-alpinejs/calendar.html , 출처: https://www.bennadel.com/blog/4620-code-kata-alpine-js-calendar-component.htm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Calendar</title> | |
<link rel="icon" href="data:,"> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js" defer></script> | |
<link rel="stylesheet" href="calendar.css" /> | |
<script src="calendar.js"></script> | |
</head> | |
<body> | |
<h1>Alpine.js Calendar</h1> | |
<div x-data="calendar_module" id="calendar-container"> | |
<button onclick="changeAvailableDates()">Change availableDates</button> | |
<br> | |
<p> | |
<label for="selection-mode">Selection mode:</label> | |
<select x-model="selectMode" id="selection-mode"> | |
<option value="single" selected>Single</option> | |
<option value="multiple">Multiple</option> | |
</select> | |
</p> | |
<p> | |
<label for="show-month-count">Show month(s) count:</label> | |
<select @change="updateShowMonthCount($event.target.value)" id="show-month-count"> | |
<template x-for="(_, index) in Array(12).fill(null)" :key="index"> | |
<option :value="index+1" x-text="index+1"></option> | |
</template> | |
</select> | |
</p> | |
<template x-if="!selectedEntry && selectedDates.length == 0"> | |
<p>Please select date.</p> | |
</template> | |
<template x-if="selectedEntry"> | |
<p> | |
Selected date: | |
<strong> | |
<a x-text="selectedDate" @click="moveToSelectedDate(selectedDate, $data)" href="javascript:void(0);"></a> | |
</strong> | |
<template x-if="selectedEntry.isToday"> | |
<span>(Today!)</span> | |
</template> | |
<button @click="selectEntry(null)">Clear</button> | |
</p> | |
</template> | |
<template x-if="selectedDates.length > 0"> | |
<p> | |
Selected dates: | |
<strong> | |
<a x-text="selectedDates[0]" @click="moveToSelectedDate(selectedDates[0], $data)" href="javascript:void(0);"></a> | |
~ | |
<a x-text="selectedDates[1]" @click="moveToSelectedDate(selectedDates[1], $data)" href="javascript:void(0);"></a> | |
</strong> | |
<button @click="selectEntry(null)">Clear</button> | |
</p> | |
</template> | |
<p class="jumper"> | |
<strong>Jump to:</strong> | |
<button @click="resetCalendar">Today</button> | |
<template x-for="(_, index) in Array(5).fill(null)" :key="index"> | |
<button @click="moveToYearCurrentMonth(getToday().year+index)" x-text="getToday().year+index"></button> | |
</template> | |
</p> | |
<!-- Calendar Begin --> | |
<div x-data="calendar_entry" class="calendar-entry-container" style="display: flex; flex-wrap: wrap;"> | |
<template x-for="(g, k) in grids" :key="k"> | |
<table class="calendar"> | |
<thead> | |
<tr> | |
<th colspan="7"> | |
<div class="tools"> | |
<button @click="gotoPrevMonth()">←</button> | |
<span x-text="monthNames[k]"></span> | |
<span x-text="years[k]"></span> | |
<button @click="gotoNextMonth()">→</button> | |
</div> | |
</th> | |
</tr> | |
<tr> | |
<template x-for="header in g.headersAbbreviated" :key="header"> | |
<th scope="col" x-text="header"> | |
</th> | |
</template> | |
</tr> | |
</thead> | |
<tbody> | |
<template x-for="(week, i) in g.weeks" :key="i"> | |
<tr> | |
<template x-for="entry in week" :key="entry.id"> | |
<td> | |
<button @click="selectEntry(entry)" :class="{ | |
current: entry.isCurrentMonth, | |
other: entry.isOtherMonth, | |
today: entry.isToday, | |
weekday: entry.isWeekday, | |
saturday: entry.isSaturday, | |
sunday: entry.isSunday, | |
selected: isSelected(entry) | |
}" :disabled="!getAvailable(entry)" x-text="entry.date"> | |
</button> | |
</td> | |
</template> | |
</tr> | |
</template> | |
</tbody> | |
</table> | |
</template> | |
</div> | |
<!-- Calendar End --> | |
</div> | |
</body> | |
<script> | |
// const availableDatesSomeDates = [ | |
// "2024-07-11", "2024-07-12", "2024-07-13", "2024-07-14", "2024-07-15", "2024-07-16", | |
// "2024-07-17", "2024-07-19", "2024-07-20", "2024-07-21", "2024-07-22", "2024-07-23" | |
// ] | |
const availableDatesEmpty = [] | |
let availableDates = [] | |
const selectMode = "single" | |
// const selectMode = "multiple" | |
function createRandomAvailableDates() { | |
const today = new Date() | |
const year = today.getFullYear() | |
const month = today.getMonth() | |
const startDate = new Date(year, month, 1) | |
const endDate = new Date(year, month + 2, 0) | |
const allDates = [] | |
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { | |
const y = d.getFullYear(); | |
const m = String(d.getMonth() + 1).padStart(2, '0') | |
const day = String(d.getDate()).padStart(2, '0') | |
allDates.push(`${y}-${m}-${day}`) | |
} | |
const availableDates = [...allDates] | |
const numDatesToRemove = Math.floor(Math.random() * (allDates.length / 2)) | |
for (let i = 0; i < numDatesToRemove; i++) { | |
const randomIndex = Math.floor(Math.random() * availableDates.length) | |
availableDates.splice(randomIndex, 1) | |
} | |
return availableDates | |
} | |
function moveToSelectedDate(date, controller) { | |
controller.moveFromDateText(date) | |
} | |
function changeAvailableDates() { | |
if (availableDates.length > 0) { | |
availableDates = availableDatesEmpty | |
} else { | |
// availableDates = availableDatesSomeDates | |
availableDates = createRandomAvailableDates() | |
} | |
const calendarModule = Alpine.$data(document.querySelector("#calendar-container")) | |
calendarModule.clearSelection() | |
calendarModule.setAvailables(availableDates) | |
alert("Available dates change: " + JSON.stringify(availableDates)) | |
} | |
document.addEventListener('alpine:initialized', () => { | |
const calendarModule = Alpine.$data(document.querySelector("#calendar-container")) | |
calendarModule.setAvailables(availableDates) | |
calendarModule.setSelectMode(selectMode) | |
calendarModule.updateShowMonthCount(document.querySelector("#show-month-count").value) | |
}) | |
</script> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let CALENDAR_WEEKDAYS = [ | |
"Sunday", | |
"Monday", | |
"Tuesday", | |
"Wednesday", | |
"Thursday", | |
"Friday", | |
"Saturday" | |
] | |
let CALENDAR_WEEKDAYS_SHORT = [ | |
"Sun", | |
"Mon", | |
"Tue", | |
"Wed", | |
"Thr", | |
"Fri", | |
"Sat" | |
] | |
let CALENDAR_MONTHS = [ | |
"January", | |
"February", | |
"March", | |
"April", | |
"May", | |
"June", | |
"July", | |
"August", | |
"September", | |
"October", | |
"November", | |
"December" | |
] | |
const CURRENT_MONTH = true | |
const OTHER_MONTH = false | |
function buildEntries(year, month) { | |
var daysInMonth = getDaysInMonth(year, month) | |
var entries = [] | |
for (var i = 1; i <= daysInMonth; i++) { | |
entries.push(buildEntry(year, month, i, CURRENT_MONTH)) | |
} | |
return entries | |
} | |
function buildEntry(year, month, date, isCurrentMonth) { | |
const timestamp = new Date(year, month, date) | |
const calendarEntry = { | |
id: timestamp.getTime(), | |
year: timestamp.getFullYear(), | |
month: timestamp.getMonth(), | |
monthName: CALENDAR_MONTHS[timestamp.getMonth()], | |
date: timestamp.getDate(), | |
day: timestamp.getDay(), | |
dayName: CALENDAR_WEEKDAYS[timestamp.getDay()], | |
isToday: getIsToday(timestamp), | |
isCurrentMonth: isCurrentMonth, | |
isOtherMonth: !isCurrentMonth, | |
isWeekday: getIsWeekday(timestamp.getDay()), | |
// isWeekend: getIsWeekend(timestamp.getDay()) | |
isSaturday: getIsSaturday(timestamp.getDay()), | |
isSunday: getIsSunday(timestamp.getDay()) | |
} | |
return calendarEntry | |
} | |
function buildGrid(entries) { | |
const grid = { | |
headers: CALENDAR_WEEKDAYS.slice(), | |
headersAbbreviated: CALENDAR_WEEKDAYS_SHORT.slice(), | |
entries: entries.slice(), | |
weeks: [] | |
} | |
for (; ;) { | |
const entry = grid.entries.at(0) | |
if (getIsFirstEntryOfWeek(entry)) { break } | |
grid.entries.unshift(buildEntry(entry.year, entry.month, (entry.date - 1), OTHER_MONTH)) | |
} | |
for (; ;) { | |
const entry = grid.entries.at(-1) | |
if (getIsLastEntryOfWeek(entry)) { break } | |
grid.entries.push(buildEntry(entry.year, entry.month, (entry.date + 1), OTHER_MONTH)) | |
} | |
// Slice each entries as array per weeks | |
for (let i = 0; i < grid.entries.length; i += 7) { | |
grid.weeks.push(grid.entries.slice(i, (i + 7))) | |
} | |
return grid | |
} | |
function getDaysInMonth(year, month) { | |
const lastDayOfMonth = new Date(year, (month + 1), 0) | |
return lastDayOfMonth.getDate() | |
} | |
function getIsFirstEntryOfWeek(entry) { | |
return (entry.day == 0) | |
} | |
function getIsLastEntryOfWeek(entry) { | |
return (entry.day == 6) | |
} | |
function getIsToday(date) { | |
const timestamp = new Date() | |
const isToday = ( | |
(date.getFullYear() == timestamp.getFullYear()) && | |
(date.getMonth() == timestamp.getMonth()) && | |
(date.getDate() == timestamp.getDate()) | |
) | |
return isToday | |
} | |
function getIsWeekday(day) { | |
return !getIsWeekend(day) | |
} | |
function getIsWeekend(day) { | |
return ((day == 0) || (day == 6)) | |
} | |
function getIsSaturday(day) { | |
return (day == 6) | |
} | |
function getIsSunday(day) { | |
return (day == 0) | |
} | |
function getYMD(year, month, date) { | |
month = (month + 1).toString().padStart(2, '0') | |
date = date.toString().padStart(2, '0') | |
const ymd = `${year}-${month}-${date}` | |
return ymd | |
} | |
function getYmdFromEntry(entry) { | |
const ymd = getYMD(entry.year, entry.month, entry.date) | |
return ymd | |
} | |
const CalendarEntryController = (initialYear, initialMonth) => { | |
let moreMonthCount = 1 | |
const timestamp = new Date() | |
const year = (initialYear ?? timestamp.getFullYear()) | |
const month = (initialMonth ?? timestamp.getMonth()) | |
// const monthName = CALENDAR_MONTHS[month] | |
// const entries = buildEntries(year, month) | |
// const grid = buildGrid(entries) | |
let years = [] | |
let months = [] | |
let monthNames = [] | |
let grids = [] | |
for (let i = 0; i < moreMonthCount; i++) { | |
const timestamp_appender = new Date(year, month + (i)) | |
const years_appender = timestamp_appender.getFullYear() | |
const months_appender = timestamp_appender.getMonth() | |
const monthNames_appender = CALENDAR_MONTHS[months_appender] | |
years.push(years_appender) | |
months.push(months_appender) | |
monthNames.push(monthNames_appender) | |
const entries_appender = buildEntries(years_appender, months_appender) | |
grids.push(buildGrid(entries_appender)) | |
} | |
const objectData = { | |
moreMonthCount: moreMonthCount, | |
year: year, | |
month: month, | |
// monthName: monthName, | |
// entries: entries, | |
// grid: grid, | |
years: years, | |
months: months, | |
monthNames: monthNames, | |
grids: grids, | |
getSelectedDate() { return new Date(this.year, this.month) }, | |
gotoDate(target) { | |
this.year = target.getFullYear() | |
this.month = target.getMonth() | |
this.monthName = CALENDAR_MONTHS[this.month] | |
this.entries = buildEntries(this.year, this.month) | |
this.grid = buildGrid(this.entries) | |
this.years = [] | |
this.months = [] | |
this.monthNames = [] | |
this.grids = [] | |
for (let i = 0; i < this.moreMonthCount; i++) { | |
const timestamp_appender = new Date(this.year, this.month + i) | |
const years_appender = timestamp_appender.getFullYear() | |
const months_appender = timestamp_appender.getMonth() | |
const monthNames_appender = CALENDAR_MONTHS[months_appender] | |
this.years.push(years_appender) | |
this.months.push(months_appender) | |
this.monthNames.push(monthNames_appender) | |
const entries_appender = buildEntries(years_appender, months_appender) | |
this.grids.push(buildGrid(entries_appender)) | |
} | |
}, | |
gotoNextMonth() { | |
this.gotoDate(new Date(this.year, (this.month + 1), 1)) | |
}, | |
gotoToday() { | |
this.gotoDate(new Date()) | |
}, | |
gotoPrevMonth() { | |
this.gotoDate(new Date(this.year, (this.month - 1), 1)) | |
}, | |
gotoYear(year, month) { | |
this.gotoDate(new Date(year, (month || 0), 1)) | |
} | |
} | |
return objectData | |
} | |
const CalendarModuleController = () => { | |
const objectData = { | |
selectMode: "single", | |
showMode: 1, | |
selectedEntry: null, | |
selectedDate: null, | |
selectedDates: [], | |
availables: [], | |
init() { this.$watch("selectMode", () => { this.clearSelection() }) }, | |
getToday() { | |
const now = new Date(); | |
const year = now.getFullYear(); | |
const month = String(now.getMonth() + 1).padStart(2, '0'); | |
const date = String(now.getDate()).padStart(2, '0'); | |
const result = { | |
year: year, | |
month: month, | |
date: date | |
} | |
return result | |
}, | |
clearSelection() { | |
this.selectedEntry = null | |
this.selectedDate = null | |
this.selectedDates = [] | |
}, | |
setSelectMode(mode) { this.selectMode = mode }, | |
updateShowMonthCount(count) { | |
if (count <= 0) { count = 1 } | |
this.getCalendarEntryObject().moreMonthCount = count | |
this.clearSelection() | |
this.moveToYearCurrentMonth(this.getCalendarEntryObject().year) | |
}, | |
setAvailables(availables) { this.availables = availables }, | |
getAvailable(entry) { | |
// always available if empty | |
if (!this.availables || this.availables.length == 0) { return true } | |
const year = entry.year | |
const month = (entry.month + 1).toString().padStart(2, '0') | |
const date = entry.date.toString().padStart(2, '0') | |
const ymd = `${year}-${month}-${date}` | |
return this.availables.includes(ymd) | |
}, | |
getCalendarEntryObject() { return Alpine.$data(this.$root.querySelector(".calendar-entry-container")) }, | |
moveFromDateText(dateText) { | |
const date = new Date(dateText) | |
this.moveToYearMonth(date.getFullYear(), date.getMonth()) | |
}, | |
moveToYearMonth(year, month) { this.getCalendarEntryObject().gotoYear(year, month) }, | |
moveToYear(year) { this.getCalendarEntryObject().gotoYear(year) }, | |
moveToYearCurrentMonth(year) { this.getCalendarEntryObject().gotoYear(year, this.getCalendarEntryObject().month) }, | |
resetCalendar() { this.getCalendarEntryObject().gotoToday() }, | |
isSelected(entry) { | |
let result = false | |
switch (this.selectMode) { | |
case "single": | |
result = this.selectedEntry?.id == entry.id | |
break | |
case "multiple": | |
const ymd = getYmdFromEntry(entry) | |
if (this.selectedDates.length == 1) { | |
result = this.selectedDates.includes(ymd) | |
} else if (this.selectedDates.length == 2) { | |
s1 = this.selectedDates[0] | |
s2 = this.selectedDates[1] | |
for (let date1 = new Date(s1); date1 <= new Date(s2); date1.setDate(date1.getDate() + 1)) { | |
const ymd1 = getYMD(date1.getFullYear(), date1.getMonth(), date1.getDate()) | |
if (ymd == ymd1) { | |
result = true | |
break | |
} | |
} | |
} | |
break | |
} | |
return result | |
}, | |
selectEntry(entry = null) { | |
switch (this.selectMode) { | |
case "single": | |
if (entry && this.selectedEntry && this.selectedEntry.id == entry.id) { | |
this.selectedEntry = null // Toggle same entry | |
return | |
} | |
this.selectedEntry = entry | |
this.selectedDate = null | |
if (this.selectedEntry) { this.selectedDate = getYmdFromEntry(this.selectedEntry) } | |
break | |
case "multiple": | |
if (entry == null) { | |
this.selectedDates = [] | |
return | |
} | |
let s1, s2 | |
const ymd = getYmdFromEntry(entry) | |
if (this.selectedDates.includes(ymd)) { | |
this.selectedDates = this.selectedDates.filter((item) => item != ymd) | |
return | |
} | |
if (this.selectedDates.length == 1) { | |
s1 = new Date(this.selectedDates[0]) | |
const ymd1 = new Date(ymd) | |
if (ymd1 < s1) { | |
this.selectedDates.unshift(ymd) | |
} else { | |
this.selectedDates.push(ymd) | |
} | |
} else if (this.selectedDates.length == 2) { | |
s1 = new Date(this.selectedDates[0]) | |
// s2 = new Date(this.selectedDates[1]) | |
const ymd1 = new Date(ymd) | |
if (ymd1 < s1) { | |
this.selectedDates[0] = ymd | |
} else { | |
this.selectedDates[1] = ymd | |
} | |
} else { | |
this.selectedDates = [] | |
this.selectedDates.push(ymd) | |
} | |
if (this.selectedDates.length > 1) { | |
s1 = this.selectedDates[0] | |
s2 = this.selectedDates[1] | |
for (let date1 = new Date(s1); date1 <= new Date(s2); date1.setDate(date1.getDate() + 1)) { | |
const ymd = getYMD(date1.getFullYear(), date1.getMonth(), date1.getDate()) | |
if (this.availables.length > 0 && !this.availables.includes(ymd)) { | |
this.selectedDates = [] | |
return false | |
} | |
} | |
} | |
break | |
} | |
}, | |
getSelection() { | |
result = null | |
switch (this.selectMode) { | |
case "single": | |
result = this.selectedEntry | |
break | |
case "multiple": | |
result = this.selectedDates | |
break | |
} | |
return result | |
} | |
} | |
return objectData | |
} | |
document.addEventListener("alpine:init", () => { | |
Alpine.data("calendar_module", CalendarModuleController) | |
Alpine.data("calendar_entry", CalendarEntryController) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html, | |
body { | |
margin: 0.2em; | |
padding: 0; | |
} | |
.calendar { | |
background-color: #ffffff; | |
border: 1px solid #333333; | |
border-collapse: collapse; | |
margin: 0.5em 0.5em; | |
} | |
.calendar th { | |
border: 1px solid #333333; | |
padding: 0.5em 0.6em; | |
} | |
.calendar td { | |
border: 1px solid #333333; | |
padding: 0; | |
} | |
.calendar tbody td:has(button) { | |
background-color: #bdebff; | |
} | |
.calendar tbody td button { | |
border: none; | |
background-color: #bdebff; | |
cursor: pointer; | |
display: block; | |
padding: 10px 15px; | |
width: 100%; | |
} | |
.calendar tbody td button.saturday { | |
color: #4f7bcb; | |
} | |
.calendar tbody td button.sunday { | |
color: red; | |
} | |
.calendar tbody td button.other, | |
.calendar tbody td:has(.other) { | |
background-color: #f8fdff; | |
} | |
.calendar tbody td button.today { | |
background-color: #0095d1; | |
color: #ffffff; | |
font-weight: bold; | |
} | |
.calendar tbody td button:disabled { | |
color: #bbb; | |
} | |
.calendar tbody td button.selected, | |
.calendar tbody td button:hover { | |
background-color: #fdd881; | |
/* outline: 3px solid #009fff; | |
outline-offset: -3px; */ | |
} | |
.calendar .tools { | |
align-items: center; | |
display: flex; | |
justify-content: center; | |
gap: 20px; | |
} | |
.calendar .tools button { | |
background-color: transparent; | |
border: 1px solid #0095d1; | |
border-radius: 3px; | |
color: #0095d1; | |
cursor: pointer; | |
padding: 5px 9px; | |
} | |
.calendar .tools button:first-of-type { | |
margin-right: auto; | |
} | |
.calendar .tools button:last-of-type { | |
margin-left: auto; | |
} | |
.jumper { | |
align-items: center; | |
display: flex; | |
gap: 10px; | |
} | |
.jumper button { | |
background-color: transparent; | |
border: 1px solid #0095d1; | |
border-radius: 3px; | |
color: #0095d1; | |
cursor: pointer; | |
padding: 2px 5px; | |
margin: 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment