Skip to content

Instantly share code, notes, and snippets.

@edp1096
Last active July 2, 2024 12:16
Show Gist options
  • Save edp1096/6dbadb0ff37303cb81de64407195bb0f to your computer and use it in GitHub Desktop.
Save edp1096/6dbadb0ff37303cb81de64407195bb0f to your computer and use it in GitHub Desktop.
<!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/alpinejs@3.x.x/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()">&larr;</button>
<span x-text="monthNames[k]"></span>
<span x-text="years[k]"></span>
<button @click="gotoNextMonth()">&rarr;</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>
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)
})
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