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:

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

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

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:

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

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:

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:

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

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:

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.

Rozpoczynamy od wygenerowania rzędu z nazwami dni tygodnia:

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.

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.

Ostatni krok to ustawienie zmiennej j, która będzie przechowywać całkowitą liczbę komórek na dany miesiąc.

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:

Aby wskazać aktualny dzień, wystarczy w ostatniej pętli dokonać sprawdzenia:

To koniec naszej metody tworzącej tabelę. Pozostaje ją wywołać w metodzie init:

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

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

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:

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

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?

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

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

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:

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

Tutaj po wyborze musisz zamknąć klikając poza kalendarz<>listopad 2025

Pon
Wto
Śro
Czw
Pią
Sob
Nie

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

Tutaj dodatkowo wstawię wynik do diva<>listopad 2025

Pon
Wto
Śro
Czw
Pią
Sob
Nie

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

Cały kod

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

Oraz przykładowe wywołanie:

Last updated