Tworzymy kalendarz

W tym artykule spróbujemy stworzyć prosty datepicker, czyli kalendarz przy polu, którzy pozwala pobierać datę.

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.



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.

Konstruktor kalendarza

Kalendarz będzie mógł być podpięty pod dowolną liczbę pól, dlatego nasze obiekty będziemy tworzyć w oparciu o wspólny wzór, czyli na bazie konstruktora:


function Calendar(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:

Kalendarz

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:


function Calendar = function() {
    ...

    //metoda inicjująca
    this.init = function () {
        //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 {
    background-image: url(./icon-calendar.png);
    background-position: 99% 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;
}
/* 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):

pozycjonowanie kalendarza

function Calendar = function() {
    ...

    //metoda inicjująca obiekt
    this.init = function () {
        //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;
    display: none;
    z-index: 1;
}
.calendar.calendar-show {
    display: block;
}
...

function Calendar = function() {
    ...

    //metoda inicjująca obiekt
    this.init = function () {
        ...

        //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', function() {
            this.divCnt.classList.toggle('calendar-show');
        }.bind(this));
    }
}

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ą:


function Calendar = function() {
    ...

    this.toggleShow = function() {
        this.divCnt.classList.toggle('calendar-show');
    }

    this.show = function() {
        this.divCnt.classList.add('calendar-show');
    }

    this.hide = function() {
        this.divCnt.classList.remove('calendar-show');
    }

    //metoda inicjująca obiekt
    this.init = function () {
        ...

        //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', function() {
            this.toggleShow();
        }.bind(this));
    }
}

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:


function Calendar = function() {
    ...

    //metoda inicjująca obiekt
    this.init = function () {
        ...

        this.input.addEventListener('click', function() {
            this.toggleShow();
        }.bind(this));

        document.addEventListener('click', function() {
            this.hide();
        }.bind(this));
    }
}

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:


function Calendar = function() {
    ...

    //metoda inicjująca obiekt
    this.init = function () {
        ...

        this.input.classList.add('input-calendar');
        this.input.addEventListener('click', function() {
            this.toggleShow();
        }.bind(this));

        this.input.addEventListener('click', function(e) {
            e.stopImmediatePropagation();
        });

        this.divCnt.addEventListener('click', function(e) {
            e.stopImmediatePropagation();
        });

        document.addEventListener('click', function() {
            this.hide();
        }.bind(this));
    }
}

Nasz cały kod na chwilę obecną wygląda tak:


function Calendar(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;

    this.toggleShow = function() {
        this.divCnt.classList.toggle('calendar-show');
    }

    this.show = function() {
        this.divCnt.classList.add('calendar-show');
    }

    this.hide = function() {
        this.divCnt.classList.remove('calendar-show');
    }

    //metoda inicjująca obiekt
    this.init = function () {
        //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', function() {
            this.toggleShow();
        }.bind(this));


        this.input.addEventListener('click', function(e) {
            e.stopImmediatePropagation();
        });
        this.divCnt.addEventListener('click', function(e) {
            e.stopImmediatePropagation();
        });

        document.addEventListener('click', function() {
            this.hide();
        }.bind(this));

        //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();

Jak widzisz nie za dużo się tutaj dzieje. Rozpoczynamy wypełnianie naszego kalendarza. Od najprostszych do nieco cięższych metod.

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:


function Calendar = function() {
    ...

    this.createDateText = function () {
        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
    this.init = function () {
        ...

        //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.


function Calendar = function() {
    ...

    //metoda tworząca tabele z kalendarzem
    this.createCalendarTable = function () {
        //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:


function Calendar = function() {
    ...

    //metoda tworząca tabele z kalendarzem
    this.createCalendarTable = function () {
        //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'];
        for (let i=0; i<days.length; i++) {
            const th = document.createElement('th');
            th.innerHTML = days[i];
            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.


function Calendar = function() {
    ...

    //metoda tworząca tabele z kalendarzem
    this.createCalendarTable = function () {
        //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'];
        for (let i=0; i<days.length; i++) {
            const th = document.createElement('th');
            th.innerHTML = days[i];
            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.


function Calendar = function() {
    ...

    //metoda tworząca tabele z kalendarzem
    this.createCalendarTable = function () {
        //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'];
        for (let i=0; i<days.length; i++) {
            const th = document.createElement('th');
            th.innerHTML = days[i];
            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.


function Calendar = function() {
    ...

    //metoda tworząca tabele z kalendarzem
    this.createCalendarTable = function () {
        ...

        //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:

Puste dni miesiąca

function Calendar = function() {
    ...

    //metoda tworząca tabele z kalendarzem
    this.createCalendarTable = function () {
        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'];
        for (let i=0; i<days.length; i++) {
            const th = document.createElement('th');
            th.innerHTML = days[i];
            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:


function Calendar = function() {
    ...

    //metoda tworząca tabele z kalendarzem
    this.createCalendarTable = function () {
        ...

        //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:


function Calendar(input) {
    //metoda tworząca tabele z kalendarzem
    this.createCalendarTable = function () {
        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'];
        for (let i=0; i<days.length; i++) {
            const th = document.createElement('th');
            th.innerHTML = days[i];
            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
    this.init = function () {
        ...

        //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()


function Calendar(input, options) {
    ...

    //metoda tworząca guziczki
    this.createButtons = function () {
        //tworzę przycisk "Poprzedni miesiąc"
        const buttonPrev = document.createElement('button');
        buttonPrev.innerText = '<';
        buttonPrev.type = "button";
        buttonPrev.classList.add('input-prev');
        buttonPrev.addEventListener('click', function () {
            this.month--;
            if (this.month < 0) {
                this.month = 11;
                this.year--;
            }
            this.createCalendarTable();
            this.createDateText();
        }.bind(this));
        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', function () {
            this.month++;
            if (this.month > 11) {
                this.month = 0;
                this.year++;
            }
            this.createCalendarTable();
            this.createDateText();
        }.bind(this));
        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. By w podpiętych eventach this wskazywało na nasz obiekt kalendarza, a nie na kliknięty przycisk użyliśmy metody bind().

Jak w poprzednich przypadkach powyższą metodę musimy odpalić w metodzie init():


function Calendar(input, options) {
    ...

    //metoda tworząca guziczki
    this.createButtons = function () {
        ...
    };

    this.init = function() {
        ...
        //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:


function Calendar() {
    this.createCalendarTable = function () {
        ...
    }

    this.bindTableDaysEvent = function() {
        this.divTable.addEventListener('click', function(e) {

            if (e.target.tagName.toLowerCase() === 'td' && e.target.classList.contains('day')) {
                alert(e.target.dayNr + '-' + (this.month + 1) + '-' + this.year);
            }

        }.bind(this));
    };

    ...
}

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():


function Calendar() {
    ...

    this.bindTableDaysEvent = function () {
        ...
    }

    this.init = function() {
        ...

        //tworzymy div z tabelą.calendara
        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 (za pomocą metody Object.assign()), 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ę.


function Calendar(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():


function Calendar(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);

    ...

    this.bindTableDaysEvent = function() {
        this.divTable.addEventListener('click', function(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();
                }
            }
        }.bind(this));
    }

    ...
}

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:


function Calendar(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 : function(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;
        }.bind(this)
    }
    this.options = Object.assign({}, defaultOptions, options);

    ...

    this.bindTableDaysEvent = function() {
        this.divTable.addEventListener('click', function(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);
            }
        }.bind(this));
    }

    ...
}

Od tej pory przy tworzeniu naszych kalendarzy programista może każdorazowo zmieniać ich opcje:


function Calendar(input, options) {
    ...
}



//dla 1 inputa wyłączam tylko zamykanie przy wyborze daty
//akcji po wyborze daty nie zmieniam bo mi odpowiada (ale dalej można zamknąć klikając na input czy poza kalendarz)
const inp1 = document.querySelector('.input-demo1');
const opt1 = {
    closeOnSelect : false
}
const cal1 = new Calendar(inp1, opt1);
cal1.init();


function Calendar(input, options) {
    ...
}



//dla 2 inputa nadpisuję tylko akcję po wyborze daty
//domyślne zamykanie tutaj mi pasuje
const input2 = document.querySelector('.input-demo2');
const opt2 = {
    onDateSelect : function(day, month, year) {
        const dayText = (day < 10) ? '0' + day : day;
        const monthText = (month < 10) ? '0' + month : month;

        const date = dayText + ' / ' + monthText + ' / ' + year;
        document.querySelector('.demo7-text').innerHTML = 'Wybrałeś właśnie datę: <strong>' + date + '</strong>';
        inp2.value = date;
    }
}
const cal2 = new Calendar(inp2, opt2);
cal2.init();

Cały listing

Gdybyś zgubił się w powyższym tekście (nie dziwne), poniżej zamieszczam cały listing:


function Calendar(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 : function(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;
        }.bind(this)
    }
    this.options = Object.assign({}, defaultOptions, options);

    //metoda tworząca przyciski prev-next
    this.createButtons = function () {
        const buttonPrev = document.createElement('button');
        buttonPrev.innerText = '<';
        buttonPrev.type = "button";
        buttonPrev.classList.add('input-prev');
        buttonPrev.addEventListener('click', function () {
            this.month--;
            if (this.month < 0) {
                this.month = 11;
                this.year--;
            }
            this.createCalendarTable();
            this.createDateText();
        }.bind(this));
        this.divButtons.appendChild(buttonPrev);

        const buttonNext = document.createElement('button');
        buttonNext.classList.add('input-next');
        buttonNext.innerText = '>';
        buttonNext.type = "button";
        buttonNext.addEventListener('click', function () {
            this.month++;
            if (this.month > 11) {
                this.month = 0;
                this.year++;
            }
            this.createCalendarTable();
            this.createDateText();
        }.bind(this));
        this.divButtons.appendChild(buttonNext);
    };

    //metoda wypisująca nazwę miesiąca i roku
    this.createDateText = function () {
        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
    this.createCalendarTable = function () {
        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'];
        for (let i=0; i<days.length; i++) {
            const th = document.createElement('th');
            th.innerHTML = days[i];
            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
    this.bindTableDaysEvent = function() {
        this.divTable.addEventListener('click', function(e) {
            if (e.target.tagName.toLowerCase() === 'td' && e.target.classList.contains('day')) {
                const month2 = ((this.month + 1) < 10) ? "0" + (this.month + 1) : this.month + 1;

                if (this.options.closeOnSelect) {
                    this.hide();
                }
                this.options.onDateSelect(e.target.dayNr, this.month + 1, this.year);
            }
        }.bind(this));
    }

    //metoda ukrywa/pokazuje kalendarz
    this.toggleShow = function() {
        this.divCnt.classList.toggle('calendar-show');
    }

    //metoda pokazuje kalendarz
    this.show = function() {
        this.divCnt.classList.add('calendar-show');
    }

    //metoda ukrywa kalendarz
    this.hide = function() {
        this.divCnt.classList.remove('calendar-show');
    }

    //metoda inicjująca obiekt
    this.init = function () {
        //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');

        //podpinamy zdarzenia do pokazywania/ukrywania kalendarza
        this.input.addEventListener('click', function() {
            this.toggleShow();
        }.bind(this));

        this.divCnt.addEventListener('click', function(e) {
            e.stopImmediatePropagation();
        });
        this.input.addEventListener('click', function(e) {
            e.stopImmediatePropagation();
        });
        document.addEventListener('click', function() {
            this.hide();
        }.bind(this));
    };
};

Oraz przykładowe wywołanie:


const input = document.querySelector('.input-date');
const cal = new Calendar(btn2, {
    closeOnSelect : true,
    onDateSelect : function(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();

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać:

  1. Przerób powyższy kod na klasy w ES6
    
                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 : function(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;
                            }.bind(this)
                        }
                        this.options = Object.assign({}, defaultOptions, options);
    
                        //metodę init moglibyśmy wrzucić do konstruktora...
                        //ale dzięki temu będziemy mogli ją w przyszłości nadpisać
                        this.init();
                    }
    
                    //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', function () {
                            this.month--;
                            if (this.month < 0) {
                                this.month = 11;
                                this.year--;
                            }
                            this.createCalendarTable();
                            this.createDateText();
                        }.bind(this));
                        this.divButtons.appendChild(buttonPrev);
    
                        const buttonNext = document.createElement('button');
                        buttonNext.classList.add('input-next');
                        buttonNext.innerText = '>';
                        buttonNext.type = "button";
                        buttonNext.addEventListener('click', function () {
                            this.month++;
                            if (this.month > 11) {
                                this.month = 0;
                                this.year++;
                            }
                            this.createCalendarTable();
                            this.createDateText();
                        }.bind(this));
                        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'];
                        for (let i=0; i<days.length; i++) {
                            const th = document.createElement('th');
                            th.innerHTML = days[i];
                            tr.appendChild(th);
                        }
                        tab.appendChild(tr);
    
                        //pobieramy wszystkie 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();
                        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);
                    };
    
                    //podpinamy klik pod dni w tabeli kalendarza
                    bindTableDaysEvent() {
                        this.divTable.addEventListener('click', function(e) {
                            if (e.target.tagName.toLowerCase() === 'td' && e.target.classList.contains('day')) {
                                const month2 = ((this.month + 1) < 10) ? "0" + (this.month + 1) : this.month + 1;
    
                                if (this.options.closeOnSelect) {
                                    this.hide();
                                }
                                this.options.onDateSelect(e.target.dayNr, this.month + 1, this.year);
                            }
                        }.bind(this));
                    }
    
                    //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');
    
                        //podpinamy zdarzenia do pokazywania/ukrywania kalendarza
                        this.input.addEventListener('click', function() {
                            this.toggleShow();
                        }.bind(this));
    
                        this.divCnt.addEventListener('click', function(e) {
                            e.stopImmediatePropagation();
                        });
                        this.input.addEventListener('click', function(e) {
                            e.stopImmediatePropagation();
                        });
                        document.addEventListener('click', function() {
                            this.hide();
                        }.bind(this));
                    };
                };
                
  2. Nasz kalendarz to taki początek. Może sam wymyślisz dodatkowe funkcjonalności które się tutaj przydadzą? Kilka pomysłów które przychodzą mi do głowy:

    - możliwość podania w options nazw nagłówków dni
    - możliwość w options podania czy w kalendarzu zaznaczony obecny dzień?
    - dodatkowa funkcjonalność, która przed dodaniem klasy do naszego inputa (co robimy w metodzie init()) zamieni go z input:date na input:text (jeżeli nasz kalendarz zostanie podpięty pod input:number, input:date itp to nie będzie pewnie zbyt dobrze działał)
    - funkcjonalność, która doda do inputa dodatkową właściwość np. calendarExist. Na początku metody init sprawdzilibyśmy czy taka właściwość istnieje. Jeżeli istnieje, wtedy nie ma sensu jeszcze raz dodawać dla tego samego inputa kalendarza.
    - zamiana nazwy miesiąca i roku na odpowiednio ostylowane selekty które umożliwiały by szybki wybór miesiąca i roku
    - dodatkowy przycisk "today" w footerze kalendarza, który od razu by przenosił do aktualnego miesiąca
    Tutaj zaimplementowałem powyższe pomysły oraz dodałem opcje takie jak "czy pokazywać przycisk [dziś]" czy "dodawanie lat do selekta z wyborem roku". Do kodu dodałem komentarze.