Tworzymy.calendar

W dzisiejszym odcinku przygód Fantomasa... postaramy się stworzyć.calendar. Moim zdaniem najłatwiejszą i najlepszą metodą, jest "nie wymyślać koła na nowo" i skorzystać po prostu z ( odpowiedniego pluginu ). My jednak chcemy się uczyć, a nie iść na łatwiznę.

Zobacz DEMO naszych kalendarzowych inputów

Ogólnie rzecz biorąc - trochę sobie popiszemy.

Założenia

Przed przystąpieniem do pracy ustalmy kilka punktów, które powinniśmy spełnić:

  1. Aby nie popaść w chaos,.calendar powinien być napisany obiektowo
  2. wygląd.calendara nas nie interesuje - zajmie się tym CSS
  3. cały.calendar będzie tworzony dynamicznie w momencie wywołania, i usuwany gdy nie będzie potrzebny
  4. po wybraniu daty będzie ona gdzieś zwracana

To są nasze ogólne postanowienia. Jesteśmy jednak super, więc podnieśmy nieco poprzeczkę.
Stwórzmy nasz.calendar jako metoda dla każdego pola tekstowego. Gdy dla takiego pola wywołamy stworzoną przez nas metodę, zamieni się ono w specjalne pole z ikonką.calendara obok, która będzie wywoływać.calendar do wybrania daty. Dodatkowo nasza metoda nieco zmieni wygląd i zachowanie takiego pola.

pole.calendara

Przygotowania środowiska

Naszą pracę zaczniemy od przygotowania środowiska do pracy. Będą nam potrzebne dwie dodatkowe metody - insertAfter wstawiająca element za elementem (dla ikony kalendarza), oraz empty - służąca do czyszczenia danego elementu z jego dzieci ( obie metody poznaliśmy w rozdziale o hierarchii dokumentu ). Nasz.calendar będziemy mogli wypozycjonować absolutnie względem całego dokumentu. Moglibyśmy zawsze wykorzystać do tego dodatkowy span (z position:relative) leżący za naszym tekstowym polem i względem niego pozycjonować nasz.calendar, jednak moglibyśmy mieć wtedy problemy z wszechstronnym wykorzystaniem naszego kalendarza, gdyby w layoucie stosowany był overflow:hidden. Oczywiście zawsze możemy to zmienić - poniższy przykład jest tylko wymysłem Fantomasa.


//metoda wstawiająca element za elementem
Node.prototype.insertAfter = function(newNode) {
    if(this.nextSibling) { //jeżeli dany element ma za sobą jakiś obiekt
        return this.parentNode.insertBefore(newNode, this.nextSibling); //to wstawiamy przed tym obiektem nasz element
    } else {
        return this.parentNode.appendChild(newNode);
    }
}

//metoda czyszcząca element z jego dzieci
Node.prototype.empty = function() {
    if (this.childNodes.length) {
        for (var x=this.childNodes.length-1; x>=0; x--) {
            this.removeChild(this.childNodes[x]);
        }
    }
}

Podstawowa klasa kalendarza

Zacznijmy od stworzenia podstawowej klasy kalendarza, którą to będziemy następnie rozwijać o nowe metody i właściwości:


_Calendar = function(_obW, _class) {
    this.now = new Date();
    this.day = this.now.getDate();
    this.month = this.now.getMonth();
    this.year = this.now.getFullYear();
    this.input = _obW;
    this.textInput = _obW.previousSibling;
    this.o = this;
}

Dzięki input będziemy wiedzieli, co utworzyło nasz.calendar (a będzie to ikona kalendarza). Właściwość o wskazuje na sam obiekt - wykorzystamy to w pod obiektach . W atrybucie "_class" przekażemy klasę, jaką otrzyma nasz.calendar. Właściwość textInput będzie wskazywała na pole tekstowe, przy którym umieściliśmy naszą ikonkę kalendarza. Pamiętajmy, że nasz.calendar tworzy ikonka, nie samo pole tekstowe.

Nasz.calendar będzie zawierał wszystko co.calendar powinien zawierać - wypisane dni (tabela), guziki do przewijania oraz nazwę miesiąca. Rozwijamy więc naszą klasę:


_Calendar = function(_obW,_class) {
    this.now = new Date();
    this.day = this.now.getDate();
    this.month = this.now.getMonth();
    this.year = this.now.getFullYear();
    this.input = _obW;
    this.textInput = _obW.previousSibling;
    this.divPlace = null;
    this.divCalendarTable = null;
    this.divMonthName = null
    this.divButtons = null;
    this.o = this;
}

Właściwość divCalendarTable będzie zawierała tabelę z kalendarza. Właściwość divMonthName będzie elementem div, w którym wypiszemy nazwę miesiąca. Właściwość divPlace to div, w który wszystko upchniemy i będziemy go pozycjonować absolutnie. divButtons będzie miejscem z guzikami.

Metoda init()

Pierwszą metodą będzie metoda inicjująca nasz.calendar. To właśnie w niej zamienimy powyższe null na coś bardziej sensownego. Poza tym będziemy wiedzieli jakie metody musimy stworzyć :)

this.init = function() {
    //tworzymy div z całą zawartością
    this.divPlace = document.createElement('div');
    this.divPlace.className = _class;
    this.divPlace.style.position = "absolute"; //nie ważne jak ma klasa - musi być absolutnie :)
    this.divPlace.style.top = parseInt(this.textInput.offsetTop + this.textInput.offsetHeight) + 'px';
    this.divPlace.style.left = this.textInput.offsetLeft + 'px';                
    
    //tworzymy div z guzikami
    this.divButtons = document.createElement('div');
    this.divButtons.className = "guziki-prev-next"
    this.divPlace.appendChild(this.divButtons);
    this.createButtons();
    
    //tworzymy div z nazwą miesiąca
    this.divMonthName = document.createElement('div');
    this.divMonthName.className = 'month-name';
    this.divPlace.appendChild(this.divMonthName);
    this.createMonthName();
    
    //tworzymy div z tabelą kalendarza
    this.divCalendarTable = document.createElement('div');
    this.divCalendarTable.className = 'calendar-table-cnt';
    this.divPlace.appendChild(this.divCalendarTable);        
    this.createCalendarTable();
    
    //nasz div z zawartością wrzucamy na koniec body
    document.getElementsByTagName('body')[0].appendChild(this.divPlace);
}
this.init(); //przy stworzeniu naszego obiektu od razu odpalamy funkcję inicjującą

W metodzie inicjującej stworzyliśmy poszczególne divy z naszego kalendarza, przypisaliśmy im odpowiednie klasy, a następnie dla każdego diva, wywołaliśmy odpowiednią metodę, która tworzy jego zawartość. Te metody napiszemy już za chwilę. Pozycję naszego divPlace (zawierającego wszystkie elementy) ustaliliśmy za pomocą właściwości offset dla pola tekstowego. Dzięki temu nasz.calendar będzie się znajdował tuż pod polem tekstowym.

Metoda createButtons()

Przystępujemy do pisania poszczególnych metod. Pierwsza z nich to createButtons():


this.createButtons = function() {
    var ob = this.o;
    var buttonPrev = document.createElement('input');
        buttonPrev.value = '<';
        buttonPrev.type = "button";
        buttonPrev.className = 'input-prev';
        buttonPrev.onclick = function() {
            ob.month--;                                                
            if (ob.month<0) ob.month = 11;
            ob.createCalendarTable();
            ob.createMonthName();
        }
    this.divButtons.appendChild(buttonPrev); 
                
    var buttonNext = document.createElement('input');
        buttonNext.className = 'input-next';
        buttonNext.value = '>';
        buttonNext.type = "button";
        buttonNext.onclick = function() {
            ob.month++;                                                
            if (ob.month>11) ob.month = 0;
            ob.createCalendarTable();
            ob.createMonthName();
        }
    this.divButtons.appendChild(buttonNext);  
}

Stworzyliśmy 2 przyciski. Dodatkowo napisaliśmy im obsługę zdarzenia onclick, z wykorzystaniem techniki przekazania obiektu rodzica . W zasadzie nie ma tu nic trudnego. Obsługujemy zwykłą zmianę miesięcy, a po każdej takiej zmianie, tworzymy na nowo tabelę z kalendarza oraz wypisujemy nazwę miesiąca. Pamiętajmy, że miesiące w javascript zawierają się w zakresie 0 - 11.

Metoda createMonthName()

Kolejną metodą, którą utworzymy będzie metoda createMonthName() służąca do wypisania nazwy aktualnie wyświetlanego miesiąca:


this.createMonthName = function() {
    var monthNames = ['styczeń', 'luty', 'marzec', 'kwiecień', 'maj', 'czerwiec', 'lipiec', 'sierpień', 'wrzesień', 'październik', 'listopad', 'grudzień'];
    
    //tworzymy nazwę miesiąca
    var month = document.createTextNode(monthNames[this.month])
    
    this.divMonthName.empty(); //czyścimy zawartość diva
    this.divMonthName.appendChild(month);
}

Div z nazwą miesiąca wyczyściliśmy z jego dzieci, a następnie wstawiliśmy do niego tekst - nazwę miesiąca pobraną z tabeli.

Metoda createCalendarTable()

Wszystko było do tej pory proste. Wchodzimy więc na level Hard.

Kolejną metodą, którą się zajmiemy jest metoda createCalendarTable(), która będzie tworzyła tabelę z kalendarza.

Aby wyświetlić dni miesiąca, musimy mieć kilka danych. Miesiąc może się zacząć od poniedziałku, wtorku itp. Musimy to wyliczyć. Podobnie z liczbą dni. Jak wiemy, miesiąc może mieć 30 lub 31 dni. Dodatkowo aby nie było za łatwo, luty ma ich 28 lub 29 - zależnie od tego czy rok jest przestępny, czy nie. To także musimy wyliczyć.

Do obliczenia liczby dni wykorzystamy dodatkową metodę getDays()


this.getDays = function()  {
    var days;
    var month = this.month+1;
    if (month==1 || month==3 || month==5 || month==7 || month==8 || month==10 || month==12)  {
        days = 31;
    } else if (month==4 || month==6 || month==9 || month==11) {
        days = 30;
    } else if (month==2) {
        if (this.leapYear()) {
            days = 29;
        } else {
            days = 28; 
        }
    }
    return dni;
}

Powyższą metodę możemy napisać oczywiście o wiele krócej:


this.getDays = function()  {
    var month = this.month+1;
    var daysNumber = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    daysNumber[2] = this.leapYear() ? 29 : 28;
    return daysNumber[month];
}

Działanie tej metody polega na sprawdzeniu numeru miesiąca i zwróceniu odpowiedniej liczby dni. W przypadku lutego (linijka 9), musimy wyliczyć, czy mamy do czynienia z rokiem przestępnym. Stwórzmy więc jeszcze jedną metodę, według informacji zawartych na Wikipedii:


this.leapYear = function() {
    if (((this.year % 4)==0) && ((this.year % 100) != 0) || ((this.year % 400)==0)) {
        return true;
    } else { 
        return false; 
    }
}

Mając przygotowane powyższe metody, przystąpmy do pisania metody createCalendarTable()

       
this.createCalendarTable = function() {    
    var ob = this.o;
    var firstDay = new Date(this.year, this.month+1, 1);
    var startPosition = firstDay.getDay();                            
    var dni = this.getDays();        
    dni += startPosition;                    

    //dalszy kod metody rysującej tabelę
}

Pod zmienną ob podstawiamy obiekt, gdyż w tworzonej tabeli będą przyciski ze zdarzeniami click. Dni wyliczamy dodając do nich pozycję startową. Dalsza część tej metody polega na wykonaniu kilku pętli. Pierwsza z nich to utworzenie nazw dni. Nie specjalnego. Druga tworzy puste komórki tabeli - od 0 do pozycji pierwszego dnia w miesiącu (startPosition). Trzecia - ostatnia pętla tworzy komórki z dniami. Każda taka komórka ma przypisaną klasę "dzień", oraz przy kliknięciu zwraca do pola tekstowego datę. Po utworzeniu zawartości tabeli, wstawiamy ją do oczyszczonego divCalendarTable. Przy każdej pętli sprawdzamy, czy nie doszliśmy do końca tygodnia za pomocą równania (x%7==0). Jeżeli tak się dzieje, wówczas do dołączamy do tabeli aktualny wiersz, i zaczynamy tworzyć nowy.


this.createCalendarTable = function() {    
    var ob = this.o;
    var firstDayMonth = new Date(this.year, this.month, 1);
    
    var startPosition = firstDayMonth.getDay();                    
        
    var dni = this.getDays();
        dni += startPosition;                    
        
    var weekDay = ['Su','Mo','Tu','We','Th','Fr','Sa'];
    var table = document.createElement('table');
    table.className = 'calendar-table';
    
    this.divCalendarTable.empty();
    
    //pierwsza pętla - nagłówek tabeli z nazwami dni
    var tr = document.createElement('tr');
    for (var i = 0; i < weekDay.length; i++) {                    
        var th = document.createElement('th');
            th.appendChild(document.createTextNode(weekDay[i]));
            tr.appendChild(th);
    }
    table.appendChild(tr);
    
    //druga pętla - puste komórki do momentu pierwszego dnia
    var tr = document.createElement('tr');
    
    for (var j = 0; j < startPosition; j++) {                    
        if ( j%7 == 0) {
            table.appendChild(tr);
            var tr = document.createElement('tr');                                
        }                        
    
        var td = document.createElement('td');
            td.appendChild(document.createTextNode(' '));
        tr.appendChild(td);
    }
    
    //trzecia pętla - wypisujemy dni
    for (var i = startPosition; i < dni; i++) {                    
        if ( i%7 == 0 ) {
            tabela.appendChild(tr);
            var tr = document.createElement('tr');                                
        }
        var dayNr = parseInt(i-startPosition+1);
        if (dayNr < 10) dayNr = "0" + dayNr;
        var td = document.createElement('td');                        
            td.appendChild(document.createTextNode(dayNr));
            td.className = 'day';
            td.onclick = function() {
                var month = ((ob.month+1) < 10)? "0"+(ob.month+1) : ob.month+1;    
                ob.textInput.value = ob.rok + '-' + month + '-' + this.firstChild.nodeValue;
            };
        tr.appendChild(td);
        
    }
    table.appendChild(tr);
    
    this.divCalendarTable.appendChild(table);
}

Wykonaliśmy kolejno trzy pętle. Pierwsze dwie to chleb powszechni dla super bohaterów. Przypatrzmy się trzeciej pętli. Na jej początku ustawiamy wartość dnia. Dla poprawienia estetyki wstawiamy zero wiodące do numeru dnia. Podobnie postępujemy zresztą przy miesiącu. Każdej komórce w tej pętli ustawiamy klasę "day", oraz przypisujemy jej zdarzenie click. Zdarzenie to wstawia zero wiodące dla miesiąca, oraz wylicza rok. Javascript nie za bardzo sobie radzi z latami po 2000 roku zwracając je jako 100+n, dlatego musimy do tej liczby dodać 1900. Ostatnia czynność z tego zdarzenia to ustawienie naszemu polu tekstowemu odpowiedniej wartości.
Nasza metoda kończy się wstawieniem do diva divCalendarTable naszej nowo utworzonej tabeli. Włala. Praktycznie (nie zupełnie) skończyliśmy pisanie naszego obiektu kalendarza.

Pozostają nam 2 rzeczy do zrobienia. Pierwsza z nich to napisanie stosownej metody dla inputów, która będzie odpalała nasz.calendar.

Metoda dla inputów

Metoda powinna utworzyć przy danym inpucie ikonkę.calendara, po kliknięciu której stworzy się nasz obiekt.calendar.


Node.prototype.calendar = function() {
    if (this.nodeName.toUpperCase()=='INPUT' && this.type.toUpperCase()=='TEXT') {
        this.value = "rrrr-mm-dd";
        this.calendarType = true;            
        var input = document.createElement('input');
            input.type = 'button';
            input.className = 'calendar-icon';
            input.calendarExist = false;
            input.calendar = null;
            input.onclick = function() {
                if (!this.calendarExist) {
                    this.calendar = new _Calendar(this, 'calendar')
                    this.calendarExist = true;
                    return
                } else {
                    this.calendar.deleteCalendar();
                    this.calendarExist = false;
                }    return
                
            }
        this.insertAfter(input);
    }
}

Sprawdzamy czy nasz input jest polem tekstowym (dla innych nie było by sensu zwracać wartości). Jeżeli jest, działamy dalej. Ustawiamy mu informacyjną wartość, oraz dodajemy mu właściwość kalendarza. Dzięki niej będziemy mogli w przyszłości łatwo wykryć, czy dany input jest już kalendarzowy. następnie tworzymy nowy input - naszą ikonkę kalendarza. Ustawiamy jej klasę "calendar-icon", oraz przypisujemy obsługę zdarzenia click. Dodajemy jej też dodatkową właściwość, w której będziemy trzymać stan istnienia kalendarza. Prosta sprawa - jeżeli dla danego inputa kalendarz nie istnieje, to go tworzymy. Jeżeli jednak istnieje, to go usuwamy. Zaraz, zaraz - przecież nasz.calendar, jeszcze się nie potrafi usuwać. No to go tego nauczmy pisząc dla niego małą metodę:


this.deleteCalendar = function() {
    this.divPlace.parentNode.removeChild(this.divPlace);
    delete this.o;
}

Od tej chwili zmiana zwykłego inputa na input z kalendarzem będzie dziecinnie prosta:


var input = document.querySelectorAll('#inputText')[0];
input.calendar();

Wszystko razem

Czas kończyć ten odcinek Przygód Super Fantomasa. Zbierzmy wszystko razem do kupy:


//środowisko
Node.prototype.insertAfter = function(newNode) {
    if(this.nextSibling) { //jeżeli dany element ma za sobą jakiś obiekt
        return this.parentNode.insertBefore(newNode, this.nextSibling); //to wstawiamy przed tym obiektem nasz element
    } else {
        return this.parentNode.appendChild(newNode);
    }
};

Node.prototype.empty = function() {
    if (this.childNodes.length) {
        for (x=this.childNodes.length-1; x>=0; x--) {
            this.removeChild(this.childNodes[x]);
        }
    }
};   

//super klasa obiektów.calendar
_Calendar = function(_obW, _class) {
    this.now = new Date();
    this.day = this.now.getDate();
    this.month = this.now.getMonth();
    this.year = this.now.getFullYear();
    this.input = _obW;
    this.textInput = _obW.previousSibling;
    this.divPlace = null;
    this.divCalendarTable = null;
    this.divMonthName = null
    this.divButtons = null;
    this.o = this;

    //metoda tworząca guziczki
    this.createButtons = function() {
        var ob = this.o;
        var buttonPrev = document.createElement('input');
        buttonPrev.value = '<';
        buttonPrev.type = "button";
        buttonPrev.className = 'input-prev';
        buttonPrev.onclick = function() {
            ob.month--;
            if (ob.month<0) ob.month = 11;
            ob.createCalendarTable();
            ob.createMonthName();
        }
        this.divButtons.appendChild(buttonPrev); 
                    
        var buttonNext = document.createElement('input');
        buttonNext.className = 'input-next';
        buttonNext.value = '>';
        buttonNext.type = "button";
        buttonNext.onclick = function() {
            ob.month++;
            if (ob.month>11) ob.month = 0;
            ob.createCalendarTable();
            ob.createMonthName();
        };
        this.divButtons.appendChild(buttonNext);  
    }

    this.createMonthName = function() {
        var monthNames = ['styczeń', 'luty', 'marzec', 'kwiecień', 'maj', 'czerwiec', 'lipiec', 'sierpień', 'wrzesień', 'październik', 'listopad', 'grudzień'];
        
        //tworzymy nazwę miesiąca
        var month = document.createTextNode(monthNames[this.month])
        
        this.divMonthName.empty(); //czyścimy zawartość diva
        this.divMonthName.appendChild(month);
    }

    //metoda wyliczająca liczbę dni
    this.getDays = function()  {
        var dni;
        var month = this.month+1;
        if (month==1 || month==3 || month==5 || month==7 || month==8 || month==10 || month==12)  {
            days = 31;
        } else if (month==4 || month==6 || month==9 || month==11) {
            days = 30;
        } else if (month==2) {
            if (this.leapYear()) {
                days = 29;
            } else { 
                days = 28;
            }
        }
        return days;
    }

    //metoda wyliczająca rok przestępny        
    this.leapYear = function() {
        if (((this.year % 4)==0) && ((this.year % 100) != 0) || ((this.year % 400)==0)) {
            return true;
        } else { 
            return false; 
        }
    }

    //metoda tworząca.calendar
    this.createCalendarTable = function() {    
        var ob = this.o;
        var firstDayMonth = new Date(this.year, this.month, 1);

        var startPosition = firstDayMonth.getDay();                    
            
        var days = this.getDays();
        days += startPosition;
            
        var weekDay = ['Su','Mo','Tu','We','Th','Fr','Sa'];
        var table = document.createElement('table');
        table.className = 'calendar-table';

        this.divCalendarTable.empty();

        //pierwsza pętla - nagłówek tabeli z nazwami dni
        var tr = document.createElement('tr');
        for (var i = 0; i < weekDay.length; i++) {
            var th = document.createElement('th');
                th.appendChild(document.createTextNode(weekDay[i]));
                tr.appendChild(th);
        }
        table.appendChild(tr);
        
        //druga pętla - puste komórki do momentu pierwszego dnia
        var tr = document.createElement('tr');

        for (var j = 0; j < startPosition; j++) {
            if ( j%7 == 0) {                    
                tabela.appendChild(tr);
                var tr = document.createElement('tr');                                
            }                        

            var td = document.createElement('td');
                td.appendChild(document.createTextNode(' '));
            tr.appendChild(td);
        }
        
        //trzecia pętla - wypisujemy dni
        for (var i = startPosition; i < dni; i++) {
            if ( i%7 == 0 ) {
                tabela.appendChild(tr);
                var tr = document.createElement('tr');                                
            }
            var dayNr = parseInt(i-startPosition+1);
            if (dayNr < 10) dayNr = "0" + dayNr;
            var td = document.createElement('td');                        
                td.appendChild(document.createTextNode(dayNr));
                td.className = 'day';
                td.onclick = function() {
                    var month = ((ob.month+1) < 10)? "0"+(ob.month+1) : ob.month+1;    
                    ob.textInput.value = ob.year + '-' + month + '-' + this.firstChild.nodeValue;
                };
            tr.appendChild(td);
            
        }
        table.appendChild(tr);
        this.divCalendarTable.appendChild(table);
    }
    
    //metoda usuwa.calendar
    this.deleteCalendar = function() {
        this.divPlace.parentNode.removeChild(this.divPlace);
        delete this.o;
    }
        
    //metoda inicjująca obiekt
    this.init = function() {
        //tworzymy div z całą zawartością
        this.divPlace = document.createElement('div');
        this.divPlace.className = _class;
        this.divPlace.style.position = "absolute"; //nie ważne jak ma klasa - musi być absolutnie :)
        this.divPlace.style.top = parseInt(this.textInput.offsetTop + this.textInput.offsetHeight) + 'px';
        this.divPlace.style.left = this.textInput.offsetLeft + 'px';            
        
        //tworzymy div z guzikami
        this.divButtons = document.createElement('div');
        this.divButtons.className = "guziki-prev-next"
        this.divPlace.appendChild(this.divButtons);
        this.createButtons();

        //tworzymy div z nazwą miesiąca
        this.divMonthName = document.createElement('div');
        this.divMonthName.className = 'month-name';
        this.divPlace.appendChild(this.monthName);
        this.createMonthName();

        //tworzymy div z tabelą kalendarza
        this.divCalendarTable = document.createElement('div');
        this.divCalendarTable.className = 'calendar-table-cnt';
        this.divPlace.appendChild(this.divCalendarTable);        
        this.createCalendarTable();

        //nasz div z zawartością wrzucamy na koniec body
        document.getElementsByTagName('body')[0].appendChild(this.divPlace);
    }
    this.init(); //przy stworzeniu naszego obiektu od razu odpalamy funkcję inicjującą
}

//metoda dla inputów - zamienia inputy na kalendarzowe
Node.prototype.calendar = function() {
    if (this.nodeName.toUpperCase()=='INPUT' && this.type.toUpperCase()=='TEXT') {
        this.value = "rrrr-mm-dd";
        this.calendarType = true;
        var input = document.createElement('input');
        input.type = 'button';
        input.className = 'calendar-icon';
        input.calendarExist = false;
        input.calendar = null;
        input.onclick = function() {
            if (!this.calendarExist) {
                this.calendar = new _Calendar(this, 'calendar')
                this.calendarExist = true;
                return;
            } else {
                this.calendar.deleteCalendar();
                this.calendarExist = false;
            }    
            return;
        }
        this.insertAfter(input);
    }
}

Przykładowe stylowanie dla naszego dzieła:


.calendar-icon { border: 0; background: url(../../images/calendar.gif) no-repeat; width: 21px; height: 21px; position: relative; top: 2px; cursor: pointer }
.calendar { width: 250px; background: #fff; height: 260px; padding: 5px; border: 1px solid #ddd; box-shadow:2px 2px 1px rgba(0,0,0,0.1);}
.calendar .input-prev, .calendar .input-next { cursor:pointer; width: 30px; height: 20px; border: 1px solid #ddd; background: #fff; font: 11px Arial; color: #333;}
.calendar .input-prev:hover, .calendar .input-next:hover {background:#D1EBFD; }
.calendar .input-prev { float: left; }
.calendar .input-next { float: right }
.calendar .month-name { font: 11px Arial; color: #666; margin: 0 40px; text-align: center; }
.calendar .calendar-table-cnt { margin-top: 30px }
.calendar .calendar-table-cnt table { font: 12px Arial; color: #666; position: absolute; top: 0; left: 0; margin-top: 30px; width: 100%; }
.calendar .calendar-table-cnt table th { font: bold 11px Arial; color: #333; padding-top: 3px; }
.calendar .calendar-table-cnt table td {height:28px; text-align: center;}
.calendar .calendar-table-cnt table td.day { border: 1px solid #ddd; padding: 2px; font: 12px Arial }
.calendar .calendar-table-cnt table td.day:hover { background: #D1EBFD; cursor: pointer }

Plus przykładowa metamorfoza:


document.getElementById('someTextInput').onclick = function () {
    if (!document.getElementById('someTextInput').calendarExist) {
        document.getElementById('someTextInput').calendar();
    }

    if (!document.getElementById('someTextInpu2').calendarExist) {
        document.getElementById('someTextInput2').calendar();
    }
}    

Demo

Nasze inputy wyglądają następująco:


Kilka słów zakończenia

Co jeszcze można dodać?
Możemy utworzyć dodatkowy atrybut dla naszej klasy .calendar, który będzie mówił, czy.calendar ma być usuwany po wybraniu daty. Wystarczy wtedy sprawdzić wartość tego atrybutu, i jeżeli jest prawdziwy, wówczas w zdarzeniu click dla komórek wywoływać dodatkowo metodę deleteCalendar. Dodatkowym usprawnieniem może być pobieranie przy zamianie inputa jego value i ustawienie jako dodatkowej właściwości. Następnie umożliwienie odmiany zamienionego pola na zwykłe pole wraz z przywróceniem poprzedniej wartości. Myślę, że jest to na tyle proste (i podobne do tego co zrobiliśmy), że zostawię to jako praca domowa.

Na pewno też zauważyłeś pewne niekonsekwencje przy trzymaniu się techniki przekazywania obiektu do pod obiektów. W niektórych metodach to robię, w niektórych nie. Ładnym zwyczajem było by przy każdej metodzie stosować tą technikę, i nie mieszać this z this.o (nasz przykład).
Z tego co sobie przypominam, starsze przeglądarki nie za dobrze radziły sobie z obsługą offsetTop, offsetLeft i offsetHeight. Dla nich przydało by się napisać dodatkową funkcję, która robiła by pętlę po nadrzędnych obiektach i dodawała wszystkie wartości uzyskując w ten sposób pozycję od początku strony. Przykładowa pętla mogła by mieć postać:


while (object.parentNode) {
    top = object.parentNode.offsetTop;
    left += object.parentNode.offsetLeft;
    object = object.parentNode
}

Powyższy kod przetestowałem na FF, Operze i Chromie - wszystkie przeglądarki nie narzekały. Na IE6 nie testowałem, bo go już nie wspieram. Dla innych przeglądarek będziesz też musiał przerobić metody Node, które stworzyliśmy w środowisku. Zawsze możesz napisać je w postaci zwykłych funkcji (IE niestety nie rozumie czym jest Node).