Tworzymy kalendarz
W tym artykule spróbujemy stworzyć prosty datepicker, czyli kalendarz przy polu, którzy pozwala pobierać datę.
Artykuł ten traktuj jako formę treningu, poznanie kilku metod, które możesz w przyszłości wykorzystać - np. do stworzenia kalendarza o zupełnie innym wyglądzie.
W praktyce ręczne tworzenie takiego datepickera staje się trochę sztuką dla sztuki, ponieważ na rynku istnieją już naprawdę dobre pluginy do takich rzeczy - chociażby pickadate.js czy Bootstrap Datepicker.
Klasa kalendarza
Kalendarz będzie mógł być podpięty pod dowolną liczbę pól, dlatego zróbmy go na bazie klasy:
class Calendar {
constructor(input) {
this.now = new Date();
this.day = this.now.getDate();
this.month = this.now.getMonth();
this.year = this.now.getFullYear();
this.input = input; //zmienna trzymająca referencję do inputa tekstowego do którego podepniemy kalendarz
this.divCnt = null; //kontener kalendarza
this.divHeader = null; //nagłówek kalendarza
this.divTable = null; //kontener tabeli z kalendarzem
this.divDateText = null; //kontener z nazwą miesiąca i roku
this.divButtons = null; //kontener z przyciskami przełączającymi miesiąc
}
}
Pierwsze zmienne jakie tworzymy wykorzystamy przy renderowaniu dni i przełączaniu dat kalendarza.
Kolejne zmienne będą zawierały w sobie części layotu według poniższej grafiki:
Tworzenie layoutu kalendarza
Pierwszą metodą którą napiszemy będzie metoda init(), która stworzy składowe naszego kalendarza oraz odpali kilka metod. Na początku skupmy się na składowych layoutu kalendarza:
class Calendar {
...
//metoda inicjująca
init() {
//kontener całego kalendarza
this.divCnt = document.createElement("div");
this.divCnt.classList.add("calendar");
//tworzymy div zawierający przyciski prev, next
this.divButtons = document.createElement("div");
this.divButtons.className = "calendar-prev-next";
//tworzymy div z nazwą miesiąca i roku
this.divDateText = document.createElement("div");
this.divDateText.className = "date-name";
//div z przyciskami prev, next i div z nazwą miesiąca
//wrzucamy do wspólnego div .calendar-header - dla lepszej logiki
this.divHeader = document.createElement("div");
this.divHeader.classList.add("calendar-header");
this.divHeader.appendChild(this.divButtons);
this.divHeader.appendChild(this.divDateText);
this.divCnt.appendChild(this.divHeader);
//tworzymy div do którego trafi tabela kalendarza
this.divTable = document.createElement("div");
this.divTable.className = "calendar-table-cnt";
this.divCnt.appendChild(this.divTable);
}
}
Od razu dodajmy stylowanie dla tych elementów. Nie jest to kurs CSS, dlatego nie będę się tutaj skupiać na samych stylach (ale jak chcesz daj znać to się powalczy):
/* wrapper dla inputa kalendarza */
.input-calendar-cnt {
position: relative;
display: inline-block;
}
/* input kalendarza */
.input-calendar {
//ikonka wzięta ze strony https://boxicons.com/
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAnklEQVRIS2NkoDFgpLH5DMRY8B/qCHS1uMRR3Ex3CxwYGBjmMzAwKJAZdA8YGBgSGRgYDsD0o/sApECeTMNh2kBmKOKyABauFNqBiFuyIg6P7RgRP+AWEBtkMIeS7IOhbwGpqYnkIBq1AKM0JaqEHFIZbehF8gcGBgZ+Up2Npv4hcn2CXtiBKpwFFNQJIMMT8FU4FDoeUzsxdTJFlgIAdwEsGTsi/XsAAAAASUVORK5CYII=");
background-position: calc(100% - 10px) center;
background-repeat: no-repeat;
border:1px solid #ddd;
cursor: pointer;
}
/* kalendarz */
.calendar {
position: absolute;
top:100%;
left:0;
width: 300px;
background: #fff;
min-height: 300px;
padding: 5px;
border: 1px solid #ddd;
box-shadow:2px 2px 1px rgba(0,0,0,0.1);
font-family: sans-serif;
display: block;
z-index: 100;
opacity: 0;
transition: 0.4s opacity;
pointer-events: none;
}
.calendar.calendar-show {
opacity: 1;
pointer-events: all;
}
/* przyciski prev-next */
.calendar-prev-next {
height:30px;
display: flex;
justify-content: space-between;
}
.calendar .input-prev,
.calendar .input-next {
position: relative;
z-index: 1;
cursor:pointer;
width: 30px;
font-family: sans-serif;
background-color: transparent;
border:0;
color: #333;
}
.calendar .input-prev:hover,
.calendar .input-next:hover {
background:#D1EBFD;
}
/* nazwa miesiąca i roku */
.calendar .date-name {
font-size:12px;
color: #666;
padding: 0 40px;
text-align: center;
line-height: 30px;
position: absolute;
left:0;
top:0;
width:100%;
text-align: center;
box-sizing: border-box;
}
/* nagłówek kalendarza */
.calendar .calendar-header {
height:30px;
position: relative;
}
/* tabela kalendarza */
.calendar .calendar-table-cnt {
position: relative;
padding-top:10px;
}
.calendar .calendar-table {
font-size:12px;
color: #666;
width: 100%;
}
.calendar .calendar-table td {
text-align: center;
width:14.2857%;
}
.calendar .calendar-table th {
font-family: sans-serif;
font-weight: normal;
padding-bottom: 10px;
font-size:11px;
color:#aaa;
}
.calendar .calendar-table td.day {
background: #fafafa;
height:40px;
font-size:11px;
font-family: sans-serif;
font-weight:bold;
}
.calendar .calendar-table td.day:hover {
background: #D1EBFD;
cursor: pointer
}
.calendar .calendar-table td.current-day {
background-color: #F15C5C;
color:#fff;
}
.calendar .calendar-table td.current-day:hover {
background-color: #DE5858;
}
Po kliknięciu na input, kalendarz będzie się pojawiał pod takim polem (zresztą widać to na powyższym demie).
Moglibyśmy wrzucać nasz kalendarz bezpośrednio do body i pozycjonować go absolutnie pobierając pozycję inputa (offsetTop i offsetLeft), ale jest to problematyczne rozwiązanie.
Struktura formularza czy samej strony może się dynamicznie zmieniać (np. mogą powyżej naszego pola dochodzić dynamicznie generowane elementy) co zmuszało by nas do każdorazowego wyliczania pozycji kalendarza na nowo.
O wiele łatwiejszym podejściem jest pozycjonować kalendarz względem elementu, którym okryjemy nasz input (pamiętaj - pozycjonowanie absolutne jest względem pierwszego rodzica, który ma pozycjonowanie inne niż static):
class Calendar {
...
//metoda inicjująca obiekt
init() {
//zmieniany inputowi dodaję klasę - doda ona ikonkę kalendarza i zmieni jego kursor
this.input.classList.add("input-calendar");
//kontener całego kalendarza
this.divCnt = document.createElement("div");
this.divCnt.classList.add("calendar");
//tworzymy div zawierający przyciski prev, next
this.divButtons = document.createElement("div");
this.divButtons.className = "calendar-prev-next";
//tworzymy div z nazwą miesiąca i roku
this.divDateText = document.createElement("div");
this.divDateText.className = "date-name";
//div z przyciskami prev, next i div z nazwą miesiąca
//wrzucamy do wspólnego div .calendar-header - dla lepszej logiki
this.divHeader = document.createElement("div");
this.divHeader.classList.add("calendar-header");
this.divHeader.appendChild(this.divButtons);
this.divHeader.appendChild(this.divDateText);
this.divCnt.appendChild(this.divHeader);
//tworzymy div do którego trafi tabela kalendarza
this.divTable = document.createElement("div");
this.divTable.className = "calendar-table-cnt";
this.divCnt.appendChild(this.divTable);
//tworzymy wrapper dla input
this.calendarWrapper = document.createElement("div");
this.calendarWrapper.classList.add("input-calendar-cnt");
this.input.parentElement.insertBefore(this.calendarWrapper, this.input);
this.calendarWrapper.appendChild(this.input);
}
}
Wykorzystaliśmy tutaj pewną sztuczkę. Wstawiliśmy przed input nasz calendarWrapper. Następnie do tego wrappera wstawiliśmy sam input. Pojedynczy element na stronie nie może istnieć równocześnie w 2 miejscach (chyba, że jest to kopia) dlatego nasz input został przeniesiony do wrappera. Pozostało dorzucić do tego wrappera kontener z kalendarzem.
Kolejnym krokiem będzie podpięcie pokazywania kalendarza.
Po kliknięciu na input kalendarz powinien się pokazać. Zrobimy to przez dodatkową klasę, która będzie zmieniać display kalendarza:
.input-calendar-cnt {
position: relative;
}
.calendar {
position: absolute;
top:100%;
left:0;
width: 300px;
background: #fff;
min-height: 300px;
padding: 5px;
border: 1px solid #ddd;
box-shadow:2px 2px 1px rgba(0,0,0,0.1);
font-family: sans-serif;
z-index: 100;
opacity: 0;
transition: 0.4s opacity;
pointer-events: none;
}
.calendar.calendar-show {
opacity: 1;
pointer-events: all;
}
...
class Calendar {
...
//metoda inicjująca obiekt
init() {
...
//tworzymy wrapper dla input
this.calendarWrapper = document.createElement("div");
this.calendarWrapper.classList.add("input-calendar-cnt");
this.input.parentElement.insertBefore(this.calendarWrapper, this.input);
this.calendarWrapper.appendChild(this.input);
this.input.addEventListener("click", e => this.divCnt.classList.toggle("calendar-show"));
}
}
Zwykłe dołączanie lub przełączanie klasy do kontenera z kalendarzem. O wiele lepiej taki kod przenieść do oddzielnych metod, które - a nóż może kiedyś się przydadzą:
class Calendar() {
...
toggleShow() {
this.divCnt.classList.toggle("calendar-show");
}
show() {
this.divCnt.classList.add("calendar-show");
}
hide() {
this.divCnt.classList.remove("calendar-show");
}
//metoda inicjująca obiekt
init() {
...
//tworzymy wrapper dla input
this.calendarWrapper = document.createElement("div");
this.calendarWrapper.classList.add("input-calendar-cnt");
this.input.parentElement.insertBefore(this.calendarWrapper, this.input);
this.calendarWrapper.appendChild(this.input);
this.input.addEventListener("click", e => this.toggleShow());
}
}
Po kliknięciu na input nasz kalendarz się pokazuje lub ukrywa. Zwiększając użyteczność powinniśmy umożliwić użytkownikowi ukrywanie naszego kalendarza poprzez kliknięcie wszędzie poza kalendarzem. Wystarczy dodać do dokumentu nasłuchiwanie click, które ukryje kalendarz:
class Calendar {
...
//metoda inicjująca obiekt
init() {
...
this.input.addEventListener("click", e => this.toggleShow());
document.addEventListener("click", e => this.hide());
}
}
Pamiętaj, że eventy idą w górę dokumentu. Jeżeli klikniemy na kalendarz albo input, wtedy ten klik dojdzie aż do dokumentu, co spowoduje, że kalendarz się pojawi i od razu zostanie ukryty. Aby przerwać podróż takiego klika do góry dokumentu musimy zastosować stopImmediatePropagation dla inputa i kontenera z kalendarzem. Dzięki temu klikając na te elementy zdarzenie kliknięcia zatrzyma się na klikniętym elemencie:
class Calendar {
...
//metoda inicjująca obiekt
init() {
...
this.input.classList.add("input-calendar");
this.input.addEventListener("click", e => this.toggleShow());
this.input.addEventListener("click", e => e.stopImmediatePropagation());
this.divCnt.addEventListener("click", e => e.stopImmediatePropagation());
document.addEventListener("click", e => this.hide());
}
}
Nasz cały kod na chwilę obecną wygląda tak:
class Calendar {
constructor(input) {
this.now = new Date();
this.day = this.now.getDate();
this.month = this.now.getMonth();
this.year = this.now.getFullYear();
this.input = input;
this.divCnt = null;
this.divTable = null;
this.divDateText = null;
this.divButtons = null;
}
toggleShow() {
this.divCnt.classList.toggle("calendar-show");
}
show() {
this.divCnt.classList.add("calendar-show");
}
hide() {
this.divCnt.classList.remove("calendar-show");
}
//metoda inicjująca obiekt
init() {
//zmieniany inputowi dodaję klasę - doda ona ikonkę kalendarza i zmieni jego kursor
this.input.classList.add("input-calendar");
//tworzymy div z całą zawartością
this.divCnt = document.createElement("div");
this.divCnt.classList.add("calendar");
//tworzymy wrapper dla input
this.calendarWrapper = document.createElement("div");
this.calendarWrapper.classList.add("input-calendar-cnt");
this.input.parentElement.insertBefore(this.calendarWrapper, this.input);
this.calendarWrapper.appendChild(this.input);
//tworzymy div z guzikami
this.divButtons = document.createElement("div");
this.divButtons.className = "calendar-prev-next";
//tworzymy div z nazwą miesiąca
this.divDateText = document.createElement("div");
this.divDateText.className = "date-name";
this.divHeader = document.createElement("div");
this.divHeader.classList.add("calendar-header");
this.divHeader.appendChild(this.divButtons);
this.divHeader.appendChild(this.divDateText);
this.divCnt.appendChild(this.divHeader);
//tworzymy div z tabelą kalendarza
this.divTable = document.createElement("div");
this.divTable.className = "calendar-table-cnt";
this.divCnt.appendChild(this.divTable);
this.input.addEventListener("click", e => this.toggleShow());
this.input.addEventListener("click", e => e.stopImmediatePropagation());
this.divCnt.addEventListener("click", e => e.stopImmediatePropagation());
document.addEventListener("click", e => this.hide());
//nasz div z zawartością wrzucamy na koniec body
this.calendarWrapper.appendChild(this.divCnt);
}
}
const input = document.querySelector(".input-demo2");
const cal = new Calendar(input);
cal.init();
Tworzenie nazwy miesiąca i roku
Jedna z najprostszych metod naszego kalendarza - czyli wyświetlenie nazwy miesiąca i roku w elemencie divDateText. Po jej napisaniu musimy ją odpalić w metodzie init:
class Calendar {
...
createDateText() {
const monthNames = ["styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień", "październik", "listopad", "grudzień"];
this.divDateText.innerHTML = monthNames[this.month] + " " + this.year;
}
//metoda inicjująca obiekt
init() {
...
//tworzymy div z nazwą miesiąca
this.divDateText = document.createElement("div");
this.divDateText.className = "date-name";
this.createDateText();
}
...
}
Metoda ta na podstawie wcześniej stworzonych zmiennych (te z początku konstruktora) dodaje do elementu divDateText nazwę miesiąca i roku.
Tabela kalendarza
Rozpoczynamy tworzyć tabelę z dniami miesiąca. Wrzucimy ją do elementu divTable.
Tabela taka będzie dynamicznie generowana przy przełączaniu miesięcy, dlatego musimy za każdym razem czyścić zawartość takiego diva.
class Calendar {
...
//metoda tworząca tabele z kalendarzem
createCalendarTable() {
//czyścimy div z tabelą
this.divTable.innerHTML = "";
//tworzymy tabelę z dniami kalendarza
const tab = document.createElement("table");
tab.classList.add("calendar-table");
//tabelę dołączamy do div divTable
this.divTable.appendChild(tab);
...
}
...
};
Rozpoczynamy od wygenerowania rzędu z nazwami dni tygodnia:
class Calendar {
...
//metoda tworząca tabele z kalendarzem
createCalendarTable() {
//czyścimy div z tabelą
this.divTable.innerHTML = "";
//tworzymy tabelę z dniami kalendarza
const tab = document.createElement("table");
tab.classList.add("calendar-table");
//tabelę dołączamy do div divTable
this.divTable.appendChild(tab);
//tworzymy nagłówki dni
let tr = document.createElement("tr");
tr.classList.add("calendar-table-days-names");
const days = ["Pon", "Wto", "Śro", "Czw", "Pią", "Sob", "Nie"];
days.forEach(day => {
const th = document.createElement("th");
th.innerHTML = day;
tr.appendChild(th);
});
tab.appendChild(tr);
}
...
};
Pozostaje wygenerować rzędy z dniami. Zanim cokolwiek stworzymy, musimy pobrać liczbę dni dla danego miesiąca:
Jak wiesz miesiąc może mieć 31, 30 lub 28/29 dni w zależności od tego czy jest to rok przestępny czy nie.
Na szczęście JavaScript zajmie się tymi wyliczeniami za nas. Metoda getDate() zwraca aktualny dzień w miesiącu. Jeżeli użyjemy jej wraz z generowaniem określonej daty, zwróci nam dzień wygenerowanej daty. Dlatego możemy ją wykorzystać w sztuczce zwracającej liczbę dni w danym miesiącu.
class Calendar {
...
//metoda tworząca tabele z kalendarzem
createCalendarTable() {
//czyścimy div z tabelą
this.divTable.innerHTML = "";
//tworzymy tabelę z dniami kalendarza
const tab = document.createElement("table");
tab.classList.add("calendar-table");
//tabelę dołączamy do div divTable
this.divTable.appendChild(tab);
//tworzymy nagłówki dni
let tr = document.createElement("tr");
tr.classList.add("calendar-table-days-names");
const days = ["Pon", "Wto", "Śro", "Czw", "Pią", "Sob", "Nie"];
days.forEach(day => {
const th = document.createElement("th");
th.innerHTML = day;
tr.appendChild(th);
});
tab.appendChild(tr);
//pobieramy wszystkie dni w miesiącu
const daysInMonth = new Date(this.year, this.month+1, 0).getDate();
}
...
};
Następnie pobieramy pierwszy dzień miesiąca. Miesiąc może zaczynać się od 1 dnia, albo od kolejnych. W Polskim kalendarzu tydzień zaczyna się od poniedziałku. W JavaScript-owym kalendarzu (angielskim) tydzień zaczyna się od niedzieli, a kończy na poniedziałku. Stąd kolejnym krokiem jest zrobienie korekty w razie czego przestawiając pierwszy dzień na 7.
class Calendar {
...
//metoda tworząca tabele z kalendarzem
createCalendarTable() {
//czyścimy div z tabelą
this.divTable.innerHTML = "";
//tworzymy tabelę z dniami kalendarza
const tab = document.createElement("table");
tab.classList.add("calendar-table");
//tworzymy nagłówki dni
let tr = document.createElement("tr");
tr.classList.add("calendar-table-days-names");
const days = ["Pon", "Wto", "Śro", "Czw", "Pią", "Sob", "Nie"];
days.forEach(day => {
const th = document.createElement("th");
th.innerHTML = day;
tr.appendChild(th);
});
tab.appendChild(tr);
//tabelę dołączamy do div divTable
this.divTable.appendChild(tab);
//pobieramy liczbę dni w miesiącu
const daysInMonth = new Date(this.year, this.month+1, 0).getDate();
//pobieramy pierwszy dzień miesiąca
const tempDate = new Date(this.year, this.month, 1);
let firstMonthDay = tempDate.getDay();
//w JavaScript pierwszym dniem jest niedziela, a poniedziałek ostatnim
//w razie czego robimy korektę
if (firstMonthDay === 0) {
firstMonthDay = 7;
}
}
...
};
Ostatni krok to ustawienie zmiennej j, która będzie przechowywać całkowitą liczbę komórek na dany miesiąc.
class Calendar {
...
//metoda tworząca tabele z kalendarzem
createCalendarTable() {
...
//w JavaScript pierwszym dniem jest niedziela, a poniedziałek ostatnim
//w razie czego robimy korektę
if (firstMonthDay === 0) {
firstMonthDay = 7;
}
//liczba wszystkich komórek - i pustych i z dniami
const j = daysInMonth + firstMonthDay - 1;
}
...
}
Miesiąc może się zacząć od n-tego dnia, dlatego by wygenerować tabelę dla niego powinniśmy wygenerować komórki pustych dni (czyli komórki dni przed rozpoczęciem miesiąca) a następnie w kolejnej pętli wygenerować komórki dni danego miesiąca:
class Calendar {
...
//metoda tworząca tabele z kalendarzem
createCalendarTable() {
this.divTable.innerHTML = "";
//tworzymy nazwy dni
const tab = document.createElement("table");
tab.classList.add("calendar-table");
//tworzymy nagłówki dni
let tr = document.createElement("tr");
tr.classList.add("calendar-table-days-names");
const days = ["Pon", "Wto", "Śro", "Czw", "Pią", "Sob", "Nie"];
days.forEach(day => {
const th = document.createElement("th");
th.innerHTML = day;
tr.appendChild(th);
});
tab.appendChild(tr);
//pobieramy liczbę dni w miesiącu
const daysInMonth = new Date(this.year, this.month+1, 0).getDate();
//pobieramy pierwszy dzień miesiąca
const tempDate = new Date(this.year, this.month, 1);
let firstMonthDay = tempDate.getDay();
//normalnie pierwszym dniem jest niedziela, a poniedziałek ostatnim
//w razie czego robimy korektę
if (firstMonthDay === 0) {
firstMonthDay = 7;
}
//liczba wszystkich komórek - i pustych i z dniami
const j = daysInMonth + firstMonthDay - 1;
//jeżeli pierwszy dzień miesiąca nie zaczyna się od początku, tworzymy dodatkowe TR
if (firstMonthDay-1 !== 0) {
tr = document.createElement("tr");
tab.appendChild(tr);
}
//na początku generujemy puste komórki (czyli te przed rozpoczęciem miesiąca)
for (let i=0; i<firstMonthDay-1; i++) {
const td = document.createElement("td");
td.innerHTML = "";
tr.appendChild(td);
}
//generujemy komórki z dniami miesiąca
for (let i = firstMonthDay-1; i<j; i++) {
if(i % 7 === 0){
tr = document.createElement("tr");
tab.appendChild(tr);
}
const td = document.createElement("td");
td.innerText = i - firstMonthDay + 2;
td.dayNr = i - firstMonthDay + 2;
td.classList.add("day");
tr.appendChild(td);
}
}
...
}
Aby wskazać aktualny dzień, wystarczy w ostatniej pętli dokonać sprawdzenia:
class Calendar {
...
//metoda tworząca tabele z kalendarzem
createCalendarTable() {
...
//generujemy komórki z dniami miesiąca
for (let i = firstMonthDay-1; i<j; i++) {
if (i % 7 === 0){
tr = document.createElement("tr");
tab.appendChild(tr);
}
const td = document.createElement("td");
td.innerText = i - firstMonthDay + 2;
td.dayNr = i - firstMonthDay + 2;
td.classList.add("day");
if (this.year === this.now.getFullYear() && this.month === this.now.getMonth() && this.day === i - firstMonthDay + 2) {
td.classList.add("current-day")
}
tr.appendChild(td);
}
}
...
}
To koniec naszej metody tworzącej tabelę. Pozostaje ją wywołać w metodzie init:
class Calendar {
//metoda tworząca tabele z kalendarzem
createCalendarTable() {
this.divTable.innerHTML = "";
//tworzymy nazwy dni
const tab = document.createElement("table");
tab.classList.add("calendar-table");
//tworzymy nagłówki dni
let tr = document.createElement("tr");
tr.classList.add("calendar-table-days-names");
const days = ["Pon", "Wto", "Śro", "Czw", "Pią", "Sob", "Nie"];
days.forEach(day => {
const th = document.createElement("th");
th.innerHTML = day;
tr.appendChild(th);
});
tab.appendChild(tr);
//tworzymy rzędy dni
const daysInMonth = new Date(this.year, this.month+1, 0).getDate();
const tempDate = new Date(this.year, this.month, 1);
let firstMonthDay = tempDate.getDay();
if (firstMonthDay === 0) {
firstMonthDay = 7;
}
const j = daysInMonth + firstMonthDay - 1;
if (firstMonthDay - 1 !== 0) {
tr = document.createElement("tr");
tab.appendChild(tr);
}
//tworzymy puste komórki przed dniami miesiąca
for (let i=0; i < firstMonthDay - 1; i++) {
const td = document.createElement("td");
td.innerHTML = "";
tr.appendChild(td);
}
//tworzymy komórki dni
for (let i = firstMonthDay-1; i<j; i++) {
if(i % 7 === 0){
tr = document.createElement("tr");
tab.appendChild(tr);
}
const td = document.createElement("td");
td.innerText = i - firstMonthDay + 2;
td.dayNr = i - firstMonthDay + 2;
td.classList.add("day");
if (this.year === this.now.getFullYear() && this.month === this.now.getMonth() && this.day === i - firstMonthDay + 2) {
td.classList.add("current-day")
}
tr.appendChild(td);
}
tab.appendChild(tr);
this.divTable.appendChild(tab);
}
//metoda inicjująca obiekt
init() {
...
//tworzymy div z tabelą kalendarza
this.divTable = document.createElement("div");
this.divTable.className = "calendar-table-cnt";
this.divCnt.appendChild(this.divTable);
this.createCalendarTable();
...
}
}
Podłączenie przycisków
Kolejnym krokiem jaki robimy to wygenerowanie i podłączenie przycisków służących do zmieniania miesięcy. Służyć temu będzie metoda createButtons()
class Calendar {
...
//metoda tworząca guziczki
createButtons() {
//tworzę przycisk "Poprzedni miesiąc"
const buttonPrev = document.createElement("button");
buttonPrev.innerText = "<";
buttonPrev.type = "button";
buttonPrev.classList.add("input-prev");
buttonPrev.addEventListener("click", e => {
this.month--;
if (this.month < 0) {
this.month = 11;
this.year--;
}
this.createCalendarTable();
this.createDateText();
});
this.divButtons.appendChild(buttonPrev);
//tworzę przycisk "Następny miesiąc"
const buttonNext = document.createElement("button");
buttonNext.classList.add("input-next");
buttonNext.innerText = ">";
buttonNext.type = "button";
buttonNext.addEventListener("click", e => {
this.month++;
if (this.month > 11) {
this.month = 0;
this.year++;
}
this.createCalendarTable();
this.createDateText();
});
this.divButtons.appendChild(buttonNext);
};
...
};
Po kliknięciu każdego przycisku zmieniamy miesiąc i w razie czego rok. Dodatkowo odpalamy wcześniej napisane metody createCalendarTable() i createDateText() dzięki czemu widok kalendarza się aktualizuje.
Jak w poprzednich przypadkach powyższą metodę musimy odpalić w metodzie init():
class Calendar {
...
//metoda tworząca guziczki
createButtons() {
...
}
init() {
...
//tworzymy div z guzikami
this.divButtons = document.createElement("div");
this.divButtons.className = "calendar-prev-next";
this.createButtons();
...
}
}
Podpięcie eventów dla dni
Zbliżamy się szybko ku końcowi. Kolejnym krokiem będzie podpięcie eventów dla dni kalendarza.
Moglibyśmy to robić w metodzie createCalendarTable() tam gdzie generujemy kolejne komórki kalendarza.
Jest to jednak mniej optymalne rozwiązanie. Raz, że zamiast jednego wspólnego eventu mielibyśmy wiele - dla każdej komórki jeden. W naszym przypadku może to nie zaboli, bo przecież komórek jest maksymalnie 31, ale... Drugi problem wiązał by się z usuwaniem komórek. Przy zmianie miesiąca poprzez przyciski [poprzedni-następny] za każdym razem czyścimy div z tabelą i generujemy tabele na nowo, czyli każdorazowo też generujemy nowe eventy dla nowych komórek. A co ze starymi eventami dla komórek które właśnie usunęliśmy? Nasze wcześniej podpięte eventy mógłby by zalegać w pamięci, co by powodowało wyciek pamięci.
W rozdziale o eventach poznaliśmy lepszy sposób, który polega na podpięciu eventu dla rodzica. Wykorzystajmy go tutaj. W naszym przypadku rodzicem może być dla przykładu divTable:
class Calendar {
createCalendarTable() {
...
}
bindTableDaysEvent() {
this.divTable.addEventListener("click", e => {
if (e.target.tagName.toLowerCase() === "td" && e.target.classList.contains("day")) {
alert(e.target.dayNr + "-" + (this.month + 1) + "-" + this.year);
}
});
}
...
}
Po kliknięciu na dzień wypisujemy w alert dzień, miesiąc i rok. Na razie. Później wrócimy i rozbudujemy to działanie.
Tak jak z poprzednimi metodami i tą musimy odpalić w metodzie init():
class Calendar {
...
bindTableDaysEvent() {
...
}
init() {
...
//tworzymy div z tabelą kalendarza
this.divTable = document.createElement("div");
this.divTable.className = "calendar-table-cnt";
this.divCnt.appendChild(this.divTable);
this.createCalendarTable();
this.bindTableDaysEvent();
...
}
}
Opcje
W naszym kalendarzu dwie rzeczy wymagają poprawy. Po pierwsze po wybraniu daty kalendarz się nie zamyka. Czy powinien się zamykać? Jak to mawiają Amerykańscy naukowcy - i tak i nie. Najlepiej gdyby używający tego kodu mógł zdecydować. To pierwsza opcja którą chcielibyśmy móc ustawiać.
Drugą opcją jest rzecz jasna zachowanie kalendarza po wybraniu daty. Domyślnie powinno to być uzupełnienie treści inputa wybraną datą. Ale to zachowanie też powinno być możliwe do zmiany, bo może programista chciałby nie tylko wpisać do pola datę, ale może zapisać ją w innym formacie, a może wrzucić ją dodatkowo do jakiegoś elementu w html?
Podejście z przekazywaniem obiektu opcji omówiłem dokładnie tutaj. Do naszego konstruktora przekazujemy obiekt z ustawieniami, który następnie scalamy z drugim obiektem, który zawiera domyślne ustawienia. W wyniku tej operacji dostajemy złączony obiekt opcji, który ma domyślne opcje oraz te które zostały nadpisane przez programistę.
class Calendar {
constructor(input, options) {
this.now = new Date();
this.day = this.now.getDate();
this.month = this.now.getMonth();
this.year = this.now.getFullYear();
this.input = input;
this.divCnt = null;
this.divTable = null;
this.divDateText = null;
this.divButtons = null;
const defaultOptions = {
//...defaultowe opcje...
}
this.options = Object.assign({}, defaultOptions, options);
}
...
}
W wyniku działania powyższego kodu dostaniemy właściwość options, która będzie połączeniem defaultOptions oraz tego co przekaże nam programista tworząc nowy obiekt.
Jak już powiedzieliśmy nas interesują 2 opcje. Pierwsza z nich to zamykanie kalendarza jeżeli użytkownik wybierze datę (kliknie na dzień w kalendarzu). Właściwość tą wykorzystamy w metodzie podpinającej kliknięcie pod komórki tabeli czyli bindTableDaysEvent():
class Calendar {
constructor(input, options) {
this.now = new Date();
this.day = this.now.getDate();
this.month = this.now.getMonth();
this.year = this.now.getFullYear();
this.input = input;
this.divCnt = null;
this.divTable = null;
this.divDateText = null;
this.divButtons = null;
const defaultOptions = {
closeOnSelect : false,
}
this.options = Object.assign({}, defaultOptions, options);
}
...
bindTableDaysEvent() {
this.divTable.addEventListener("click", e => {
if (e.target.tagName.toLowerCase() === "td" && e.target.classList.contains("day")) {
alert(e.target.dayNr + "-" + (this.month + 1) + "-" + this.year);
if (this.options.closeOnSelect) {
this.hide();
}
}
});
}
...
}
Druga opcja to zachowanie po wyborze daty - czyli jakaś funkcja, która będzie odpalana gdy użytkownik kliknie na dzień w kalendarzu. Domyślnie wybrana data powinna być wpisywana w input. Musimy więc zdefiniować domyślną funkcję dla tej akcji. Ale i ta opcja powinna być możliwa do zmiany. Tą opcję też podpinamy w zdarzeniu click dla komórek tabeli:
class Calendar {
constructor(input, options) {
this.now = new Date();
this.day = this.now.getDate();
this.month = this.now.getMonth();
this.year = this.now.getFullYear();
this.input = input;
this.divCnt = null;
this.divTable = null;
this.divDateText = null;
this.divButtons = null;
const defaultOptions = {
closeOnSelect : false,
onDateSelect : (day, month, year) => {
const monthText = ((month + 1) < 10) ? "0" + (month + 1) : month + 1;
const dayText = (day < 10) ? "0" + day : day;
this.input.value = `${dayText}-${monthText}-${this.year}`;
}
}
this.options = Object.assign({}, defaultOptions, options);
}
...
bindTableDaysEvent() {
this.divTable.addEventListener("click", e => {
if (e.target.tagName.toLowerCase() === "td" && e.target.classList.contains("day")) {
alert(e.target.dayNr + "-" + (this.month + 1) + "-" + this.year);
if (this.options.closeOnSelect) {
this.hide();
}
this.options.onDateSelect(e.target.dayNr, this.month + 1, this.year);
}
});
}
...
}
Od tej pory przy tworzeniu naszych kalendarzy programista może każdorazowo zmieniać ich opcje:
//dla 1 inputa wyłączam tylko zamykanie przy wyborze daty
const input1 = document.querySelector("#demo1");
const calendar1 = new Calendar(input1, {
closeOnSelect : false
});
calendar1.init();
//dla 2 inputa nadpisuję tylko akcję po wyborze daty
const input2 = document.querySelector("#demo2");
const calendar2 = new Calendar(input2, {
onDateSelect(day, month, year) {
const dayText = (day < 10) ? "0" + day : day;
const monthText = (month < 10) ? "0" + month : month;
const date = `${dayText} / ${monthText} / ${year}`;
input2.value = date;
document.querySelector("#demo2text").innerHTML = `Wybrałeś właśnie datę: <strong>${date}</strong>`;
}
});
calendar2.init();
Cały kod
Gdybyś zgubił się w powyższym tekście (nie dziwne), poniżej zamieszczam cały kod:
class Calendar {
constructor(input, options) {
this.now = new Date();
this.day = this.now.getDate();
this.month = this.now.getMonth();
this.year = this.now.getFullYear();
this.input = input;
this.divCnt = null;
this.divTable = null;
this.divDateText = null;
this.divButtons = null;
const defaultOptions = {
closeOnSelect : false,
onDateSelect : (day, month, year) => {
const monthText = ((month + 1) < 10) ? "0" + (month + 1) : month + 1;
const dayText = (day < 10) ? "0" + day : day;
this.input.value = `${dayText}-${monthText}-${this.year}`;
}
}
this.options = Object.assign({}, defaultOptions, options);
}
//metoda tworząca przyciski prev-next
createButtons() {
const buttonPrev = document.createElement("button");
buttonPrev.innerText = "<";
buttonPrev.type = "button";
buttonPrev.classList.add("input-prev");
buttonPrev.addEventListener("click", e => {
this.month--;
if (this.month < 0) {
this.month = 11;
this.year--;
}
this.createCalendarTable();
this.createDateText();
});
this.divButtons.appendChild(buttonPrev);
const buttonNext = document.createElement("button");
buttonNext.classList.add("input-next");
buttonNext.innerText = ">";
buttonNext.type = "button";
buttonNext.addEventListener("click", e => {
this.month++;
if (this.month > 11) {
this.month = 0;
this.year++;
}
this.createCalendarTable();
this.createDateText();
});
this.divButtons.appendChild(buttonNext);
}
//metoda wypisująca nazwę miesiąca i roku
createDateText() {
const monthNames = ["styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień", "październik", "listopad", "grudzień"];
this.divDateText.innerHTML = monthNames[this.month] + " " + this.year;
}
//metoda tworząca tabele z kalendarzem
createCalendarTable() {
this.divTable.innerHTML = "";
//tworzymy nazwy dni
const tab = document.createElement("table");
tab.classList.add("calendar-table");
//tworzymy nagłówki dni
let tr = document.createElement("tr");
tr.classList.add("calendar-table-days-names");
const days = ["Pon", "Wto", "Śro", "Czw", "Pią", "Sob", "Nie"];
days.forEach(day => {
const th = document.createElement("th");
th.innerHTML = day;
tr.appendChild(th);
});
tab.appendChild(tr);
//pobieramy wszystkie dni danego miesiąca
const daysInMonth = new Date(this.year, this.month+1, 0).getDate();
//pobieramy pierwszy dzień miesiąca
const tempDate = new Date(this.year, this.month, 1);
let firstMonthDay = tempDate.getDay();
if (firstMonthDay === 0) {
firstMonthDay = 7;
}
//wszystkie komórki w tabeli
const j = daysInMonth + firstMonthDay - 1;
if (firstMonthDay - 1 !== 0) {
tr = document.createElement("tr");
tab.appendChild(tr);
}
//tworzymy puste komórki przed dniami miesiąca
for (let i=0; i < firstMonthDay - 1; i++) {
const td = document.createElement("td");
td.innerHTML = "";
tr.appendChild(td);
}
//tworzymy komórki dni
for (let i = firstMonthDay-1; i<j; i++) {
if(i % 7 === 0){
tr = document.createElement("tr");
tab.appendChild(tr);
}
const td = document.createElement("td");
td.innerText = i - firstMonthDay + 2;
td.dayNr = i - firstMonthDay + 2;
td.classList.add("day");
if (this.year === this.now.getFullYear() && this.month === this.now.getMonth() && this.day === i - firstMonthDay + 2) {
td.classList.add("current-day")
}
tr.appendChild(td);
}
tab.appendChild(tr);
this.divTable.appendChild(tab);
}
//podpinamy klik pod dni w tabeli kalendarza
bindTableDaysEvent() {
this.divTable.addEventListener("click", e => {
if (e.target.tagName.toLowerCase() === "td" && e.target.classList.contains("day")) {
if (this.options.closeOnSelect) {
this.hide();
}
this.options.onDateSelect(e.target.dayNr, this.month + 1, this.year);
}
});
}
//metoda ukrywa/pokazuje kalendarz
toggleShow() {
this.divCnt.classList.toggle("calendar-show");
}
//metoda pokazuje kalendarz
show() {
this.divCnt.classList.add("calendar-show");
}
//metoda ukrywa kalendarz
hide() {
this.divCnt.classList.remove("calendar-show");
}
//metoda inicjująca obiekt
init() {
//tworzymy div z całą zawartością
this.divCnt = document.createElement("div");
this.divCnt.classList.add("calendar");
//tworzymy div z guzikami
this.divButtons = document.createElement("div");
this.divButtons.className = "calendar-prev-next";
this.createButtons();
//tworzymy div z nazwą miesiąca
this.divDateText = document.createElement("div");
this.divDateText.className = "date-name";
this.createDateText();
//tworzymy nagłówek kalendarza
this.divHeader = document.createElement("div");
this.divHeader.classList.add("calendar-header");
this.divHeader.appendChild(this.divButtons);
this.divHeader.appendChild(this.divDateText);
this.divCnt.appendChild(this.divHeader);
//tworzymy div z tabelą kalendarza
this.divTable = document.createElement("div");
this.divTable.className = "calendar-table-cnt";
this.divCnt.appendChild(this.divTable);
this.createCalendarTable();
this.bindTableDaysEvent();
//tworzymy wrapper dla input
this.calendarWrapper = document.createElement("div");
this.calendarWrapper.classList.add("input-calendar-cnt");
this.input.parentElement.insertBefore(this.calendarWrapper, this.input);
this.calendarWrapper.appendChild(this.input);
this.calendarWrapper.appendChild(this.divCnt);
this.input.classList.add("input-calendar");
this.input.addEventListener("click", e => this.toggleShow());
this.input.addEventListener("click", e => e.stopImmediatePropagation());
this.divCnt.addEventListener("click", e => e.stopImmediatePropagation());
document.addEventListener("click", e => this.hide());
}
}
Oraz przykładowe wywołanie:
const input = document.querySelector(".input-date");
const cal = new Calendar(input, {
closeOnSelect : true,
onDateSelect(day, month, year) {
console.log(day);
console.log(month);
console.log(year);
const dayText = ((day + 1) < 10) ? "0" + (day + 1) : day + 1;
const monthsNames = ["styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień", "październik", "listopad", "grudzień"];
input.value = dayText + " " + monthsNames[month] + " " + year;
}
});
cal.init();
Demo
Rezultat naszych działań możesz zobaczyć poniżej. Kliknij na pole tekstowe. Potem włącz pole kalendarzowe a następnie znowu kliknij na pole tekstowe.