UNPKG

premiumcalendar

Version:

A lightweight and customizable JavaScript + CSS calendar library for modern web applications.

473 lines (401 loc) 15 kB
/* * MMK Premium Calendar v1.0 (https://premiumcalendar.malleeshkanna.com/) * Copyright 2025 Malleeshkanna (https://malleeshkanna.com) * Licensed under MIT (https://premiumcalendar.malleeshkanna.com/license) */ class MMKPremiumCalendar { constructor(container, config = {}) { this.container = document.querySelector(container); this.config = Object.assign( { type: "range", color: "#4f46e5", disablePrevDates: false, disableFutureDates: false, // NEW OPTION shape: "default", handleChange: null, onMonthChange: null, disableDays: [], datesupto: null, dateRange: "onlyRange", disabledDates: [], darkMode: false, }, config ); this.state = { currentMonth: luxon.DateTime.now().startOf("month"), selectedDates: [], }; this.prevRenderedMonth = null; this.init(); } init() { this.render(); this.attachEvents(); } render() { this.container.innerHTML = ""; // Clear existing const wrapper = document.createElement("div"); wrapper.className = "mmk-calendar-container"; if (this.config.darkMode) { wrapper.classList.add("dark"); } const header = document.createElement("div"); header.className = "calendar-header"; const prevBtn = document.createElement("button"); prevBtn.id = "prevMonth"; prevBtn.innerHTML = "<i class='left-chevron mmk-icon'></i>"; const monthSelect = document.createElement("select"); monthSelect.className = "mmk-calendar-input"; monthSelect.id = "MmkmonthSelect"; const yearInput = document.createElement("input"); yearInput.type = "number"; yearInput.className = "mmk-calendar-input"; yearInput.id = "yearInput"; const nextBtn = document.createElement("button"); nextBtn.id = "nextMonth"; nextBtn.innerHTML = "<i class='right-chevron mmk-icon'></i>"; header.append(prevBtn, monthSelect, yearInput, nextBtn); const gridWrapper = document.createElement("div"); gridWrapper.className = "calendar-grid-wrapper"; const calendarDays = document.createElement("div"); calendarDays.className = "calendar-grid fade"; calendarDays.id = "calendarDays"; gridWrapper.appendChild(calendarDays); const todayWrapper = document.createElement("div"); todayWrapper.style.marginTop = "1rem"; todayWrapper.style.textAlign = "center"; const todayBtn = document.createElement("button"); todayBtn.id = "MmktodayBtn"; todayBtn.textContent = "Today"; todayWrapper.appendChild(todayBtn); wrapper.append(header, gridWrapper, todayWrapper); this.container.appendChild(wrapper); this.updateStyles(); this.renderCalendar(); this.updateNavButtons(); this.updateMonthYearFields(); } getMonthOptions() { return luxon.Info.months("long") .map((month, i) => `<option value="${i}">${month}</option>`) .join(""); } updateStyles() { document.documentElement.style.setProperty( "--selected-color", this.config.color ); document.documentElement.style.setProperty( "--button-bg", this.config.color ); document.documentElement.style.setProperty( "--highlight-color", this.config.color ); } updateMonthYearFields() { const yearInput = this.container.querySelector("#yearInput"); const { DateTime } = luxon; const now = DateTime.now(); const minYear = this.config.disablePrevDates ? now.year : 1950; const maxYear = this.config.disableFutureDates ? now.year : this.config.datesupto ? DateTime.fromISO(this.config.datesupto).year : now.year; yearInput.min = minYear; yearInput.max = maxYear; yearInput.value = this.state.currentMonth.year; yearInput.addEventListener("change", () => { let val = parseInt(yearInput.value, 10); if (val < minYear) val = minYear; if (val > maxYear) val = maxYear; this.state.currentMonth = this.state.currentMonth.set({ year: val }); this.renderMonthOptions(); this.renderCalendar({ animate: true }); }); this.renderMonthOptions(); } renderMonthOptions() { const monthSelect = this.container.querySelector("#MmkmonthSelect"); const { DateTime, Info } = luxon; const now = DateTime.now(); const currentYear = this.state.currentMonth.year; const minMonth = this.config.disablePrevDates && currentYear === now.year ? now.month : 1; const maxMonth = this.config.disableFutureDates && currentYear === now.year ? now.month : this.config.datesupto && currentYear === DateTime.fromISO(this.config.datesupto).year ? DateTime.fromISO(this.config.datesupto).month : 12; monthSelect.innerHTML = Info.months("long") .map((month, i) => { const monthIndex = i + 1; const isDisabled = monthIndex < minMonth || monthIndex > maxMonth; return `<option value="${i}" ${ isDisabled ? "disabled" : "" }>${month}</option>`; }) .join(""); monthSelect.value = this.state.currentMonth.month - 1; monthSelect.addEventListener("change", () => { const selectedMonth = parseInt(monthSelect.value, 10) + 1; this.state.currentMonth = this.state.currentMonth.set({ month: selectedMonth, }); this.renderCalendar({ animate: true }); }); } renderCalendar({ animate = false } = {}) { const calendarDays = this.container.querySelector("#calendarDays"); const { DateTime } = luxon; if (animate) { calendarDays.classList.remove("fade-in"); void calendarDays.offsetWidth; } let start = this.state.currentMonth.startOf("month"); while (start.weekday !== 7) start = start.minus({ days: 1 }); let end = this.state.currentMonth.endOf("month"); while (end.weekday !== 6) end = end.plus({ days: 1 }); const today = DateTime.now().toISODate(); const disableDaysMap = { mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6, sun: 7, }; const disabledWeekdays = this.config.disableDays.map( (d) => disableDaysMap[d.toLowerCase()] ); let current = start; calendarDays.innerHTML = ""; ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].forEach((d) => { const div = document.createElement("div"); div.className = "day-name"; div.textContent = d; calendarDays.appendChild(div); }); while (current <= end) { const div = document.createElement("div"); div.className = "day"; div.textContent = current.day; const iso = current.toISODate(); const isToday = iso === today; const isBeforeToday = this.config.disablePrevDates && current < DateTime.now().startOf("day"); const isAfterMaxDate = (this.config.datesupto && current > DateTime.fromISO(this.config.datesupto)) || (this.config.disableFutureDates && current > DateTime.now().startOf("day")); const isDisabledDate = this.config.disabledDates.includes(iso); const isWeekdayDisabled = disabledWeekdays.includes(current.weekday); if (isToday) div.classList.add("today"); if ( isBeforeToday || isAfterMaxDate || isDisabledDate || isWeekdayDisabled ) { div.classList.add("disabled"); if (isWeekdayDisabled) div.style.color = "red"; } if ( this.config.type === "range" && this.state.selectedDates.length === 2 ) { const [startDate, endDate] = this.state.selectedDates; if (iso > startDate && iso < endDate) { div.classList.add("in-range"); } } if (this.state.selectedDates.includes(iso)) { div.classList.add("selected"); } if (this.config.shape === "circle") { div.style.borderRadius = "50%"; } div.addEventListener("click", () => this.handleDateClick(iso)); calendarDays.appendChild(div); current = current.plus({ days: 1 }); } if ( typeof this.config.onMonthChange === "function" && (!this.prevRenderedMonth || !this.state.currentMonth.hasSame(this.prevRenderedMonth, "month")) ) { const monthNumber = parseInt(this.state.currentMonth.toFormat("M")); const monthName = this.state.currentMonth.toFormat("MMM").toLowerCase(); const year = parseInt(this.state.currentMonth.toFormat("yyyy")); this.config.onMonthChange({ month: monthName, code: monthNumber, year }); } this.prevRenderedMonth = this.state.currentMonth; this.updateNavButtons(); this.updateMonthYearFields(); if (animate) { calendarDays.classList.add("fade-in"); } } handleDateClick(iso) { const { DateTime } = luxon; const date = DateTime.fromISO(iso); const disableDaysMap = { mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6, sun: 7, }; const disabledWeekdays = this.config.disableDays.map( (d) => disableDaysMap[d.toLowerCase()] ); const isBeforeToday = this.config.disablePrevDates && date < DateTime.now().startOf("day"); const isAfterMaxDate = (this.config.datesupto && date > DateTime.fromISO(this.config.datesupto)) || (this.config.disableFutureDates && date > DateTime.now().startOf("day")); const isDisabled = this.config.disabledDates.includes(iso) || disabledWeekdays.includes(date.weekday); if (isBeforeToday || isAfterMaxDate || isDisabled) return; if (this.config.type === "single") { this.state.selectedDates = [iso]; } else if (this.config.type === "range") { if (this.state.selectedDates.length < 2) { this.state.selectedDates.push(iso); this.state.selectedDates.sort(); } else { this.state.selectedDates = [iso]; } } if (typeof this.config.handleChange === "function") { let selectedOutput = [...this.state.selectedDates]; if ( this.config.type === "range" && this.state.selectedDates.length === 2 && this.config.dateRange === "all" ) { const [startStr, endStr] = this.state.selectedDates; let dates = []; let current = DateTime.fromISO(startStr); const end = DateTime.fromISO(endStr); while (current <= end) { const iso = current.toISODate(); const isWeekdayDisabled = disabledWeekdays.includes(current.weekday); if (!this.config.disabledDates.includes(iso) && !isWeekdayDisabled) { dates.push(iso); } current = current.plus({ days: 1 }); } selectedOutput = dates; } this.config.handleChange(selectedOutput); } this.renderCalendar(); } updateNavButtons() { const prevBtn = this.container.querySelector("#prevMonth"); const nextBtn = this.container.querySelector("#nextMonth"); const today = luxon.DateTime.now().startOf("day"); const prevMonthStart = this.state.currentMonth .minus({ months: 1 }) .endOf("month"); prevBtn.disabled = this.config.disablePrevDates && prevMonthStart < today; const nextMonthStart = this.state.currentMonth .plus({ months: 1 }) .startOf("month"); const datesUpto = this.config.datesupto ? luxon.DateTime.fromISO(this.config.datesupto) : null; const isBeyondDatesUpto = datesUpto && nextMonthStart > datesUpto; const isBeyondToday = this.config.disableFutureDates && nextMonthStart > today; nextBtn.disabled = isBeyondDatesUpto || isBeyondToday; } attachEvents() { this.container.querySelector("#prevMonth").addEventListener("click", () => { this.state.currentMonth = this.state.currentMonth.minus({ months: 1 }); this.renderCalendar({ animate: true }); }); this.container.querySelector("#nextMonth").addEventListener("click", () => { const nextMonthStart = this.state.currentMonth .plus({ months: 1 }) .startOf("month"); const datesUpto = this.config.datesupto ? luxon.DateTime.fromISO(this.config.datesupto) : null; const today = luxon.DateTime.now().startOf("day"); if ( (datesUpto && nextMonthStart > datesUpto) || (this.config.disableFutureDates && nextMonthStart > today) ) return; this.state.currentMonth = nextMonthStart; this.renderCalendar({ animate: true }); }); this.container .querySelector("#MmktodayBtn") .addEventListener("click", () => { const now = luxon.DateTime.now(); const todayISO = now.toISODate(); this.state.currentMonth = now.startOf("month"); this.state.selectedDates = [todayISO]; this.renderCalendar({ animate: true }); if (typeof this.config.handleChange === "function") { this.config.handleChange([todayISO]); } }); this.addSwipeListeners(); } addSwipeListeners() { const calendar = this.container.querySelector("#calendarDays"); let touchStartX = 0; let touchEndX = 0; calendar.addEventListener("touchstart", (e) => { touchStartX = e.changedTouches[0].screenX; }); calendar.addEventListener("touchend", (e) => { touchEndX = e.changedTouches[0].screenX; const deltaX = touchEndX - touchStartX; if (Math.abs(deltaX) > 50) { const today = luxon.DateTime.now().startOf("day"); if (deltaX < 0) { const nextMonthStart = this.state.currentMonth .plus({ months: 1 }) .startOf("month"); const datesUpto = this.config.datesupto ? luxon.DateTime.fromISO(this.config.datesupto) : null; if ( (datesUpto && nextMonthStart > datesUpto) || (this.config.disableFutureDates && nextMonthStart > today) ) return; this.state.currentMonth = nextMonthStart; } else { const prevMonthEnd = this.state.currentMonth .minus({ months: 1 }) .endOf("month"); if (!this.config.disablePrevDates || prevMonthEnd >= today) { this.state.currentMonth = this.state.currentMonth.minus({ months: 1, }); } else return; } this.renderCalendar({ animate: true }); } }); } } export default MMKPremiumCalendar;