JavaScript日曆控件開發

概述

在開篇之前,先附上日曆的代碼地址和演示地址,代碼是本文要分析的代碼,演示效果是本文要實現的效果
代碼地址:https://github.com/aspwebchh/javascript-control/tree/master/calendar
演示地址: https://www.chhblog.com/html/demo/calendar.html

本文的目的除了詳細說明開發一款具備基本功能的網頁日曆的方法與細節以外,還附加說明了如何合理的組織日曆特效的代碼和因此帶來的好處。

按照本文的教程開發出來的效果如下

1.png | center | 332x296

他具有選擇年月日、選擇今天、清空文本框這些日曆的基本功能,能滿足日常項目中出現的普通日期選擇需求, 算的上是五臟俱全的小麻雀。

本文主要描述JavaScript實現的細節,日曆的CSS佈局細節將被省略,有興趣的同學可閱讀calendar.css中的css代碼獲知實現方法。

此日曆特效由原生JavaScript代碼寫成,並不依賴jQuery等第三方框架。它的JavaScript代碼由三個文件構成

common.js

公用函數庫文件, 裏面的函數都是通用型的,並不僅僅和特效相關,在任何網頁特效中都可以使用它們

calendar_core.js

一個純粹的、通用的日曆特效的所有代碼,更任何其它頁面元素沒有關係,比如說用來放置日期的文本框

calendar.js

合理調用calendar_core.js中的代碼來構建一個真正可以使用的日曆特效。

關於calendar_core.js和calendar.js的說明,似乎有點令人犯迷糊,不過這不要緊,通過下面的詳細講解,會使讀者瞭解到這兩個文件中代碼作用與區別。

代碼規約

因爲JavaScript在一些常規的編程概念上沒有統一的實現方法的緣故,在介紹日曆的核心實現邏輯之情,先介紹下代碼中所有使用的容易分散讀者注意力或者造成讀者出現理解偏差的語法細節。

防止全局變量污染名稱空間

此案列的大部分代碼會被這樣一段代碼包圍起來

(function(){
 //功能代碼
})();

其實這麼做的主要目的是爲了讓變量名稱和函數的名稱全局名稱空間, 換句話說就是讓用不到它的地方看不到它。

那爲什麼這個function要被一個括號括起來,而且在這個括號後面再加上一個括號。 括號的作用很簡單,跟小學數學中所學的括號作用一樣,是用來提升運算優先級的,比如說(1+2)*3,其中(1+2)會被優先與乘法運算,返回的結果就是3。可JavaScript沒有規定,括號中必須放置四則運算表達式,括號中也可以放別的東西,比如說函數。這麼一說就好理解了

(function(){
 //功能代碼
})();

這段代碼可一被分解爲兩步, 第一個括號的作用是返回括號中的函數,第二個括號的作用是調用第一個括號返回的函數,這跟下面這段代碼是一個意思,只是合在一起可以省略函數名。

var func = (function(){
 //功能代碼
});

func();

類的實現

非ES6的JavaScript語法不支持類,但是類是不可缺少的編程元素,所幸JavaScript可以通過function關鍵字模擬類的實現。

常規的模擬方法是使用function和function的它的prototype屬性,可這麼做無法實現面向對象中private關鍵字的效果,所以我在這個案列中並沒有採用這種方法,而是使用了

function Klass(){
     this.publicFunc1 = function(){}
     this.publicFunc2= funciton(){}
     var privateFunc1 = funciton(){}
     var privateFunc2 = function(){}
}

var klass = new Klass();
Klass.publicFunc1();
Klass.publicFunc2();

這種方式模擬類的實現。

實現細節

公用函數庫 - common.js

此文件中有4個函數

addEventHandler

爲DOM對象綁定事件。因爲要兼容低版本IE,所以特地封裝成函數

removeEventHandler

移除DOM對象綁定的事件

getOffset

獲得html元素在頁面中的位置。使用場景如點擊輸入框彈出日曆時將日曆定位到文本框下方就要用到這個函數。

checkDate

檢查日期字符串格式是否合法

這4個函數的代碼在文中略去,有需要的讀者可直接查看源代碼

日曆核心類 - calendar_core.js

此文件包含日曆特效的核心功能,其中有一個函數和一個類。

函數 newCalendarID

函數代碼如下

var instanceCount = 0;

function newCalendarID(){
    return 'calender_' + ( ++instanceCount );
}

這個函數的作用是生成代表日曆DOM元素的ID。  很多時候, 一個頁面上不會只有一個日曆,如下圖

2.png | center | 356x538

所以必須要一個不重複的值作爲不同日曆HTML元素的ID,以防止JavaScript操作日曆html元素時造成衝突。 newCalendarID通過自增一個數值變量並結合一個字符串來生成日曆的ID,生成的ID格式如下

calender_1
calender_2
calender_3
calender_4

這個函數在日曆的構造函數中被調用,每次實例化一個日曆時爲日曆html元素賦予一個ID。

類 Calender

類 Calender 封裝了實現日曆功能的代碼,包括生成日曆、年份月份切換、 選擇清空日期等等。

Calender 類的公共接口如下

function Calender() {
    //事件
    this.onClear = function() {};
    this.onSetToday = function() {};
    this.onSelected = function( y, m, d ) {};
    
    //方法
    this.render = function( placeholder ) {}
    this.setDate = function( y, m, d ) {}
    this.position = function( left, top ) {}
    this.hide = function() {}
    this.contains = function( target ) {}
} 

我們通過從外到內的模式講解日曆類的實現,先講解日曆的接口,再講解代碼的細節。

首先,有有點讀者要明白,Calender類表示的就是日曆,是那個在網頁上實現日期選擇功能的日曆特效。

當實例化Calender對象並調用對象的render方法,一個日曆就被顯示在網頁上了。 代碼如下

var c = new Calender();
c.render();

render方法就是用來生成日曆特效的html元素,並將元素添加到頁面上,執行後的效果如下圖

3.png | center | 826x364

setDate  方法用來設置日曆的日期 ,接受年月日三個參數。日曆初始化時持有的日期是當前的日期,因此日曆界面上當前日期的位置被設爲選中狀態。然而,有時候我們希望被選中的日期是任意的而非只能是今天,這個時候setDate方法就能派上用場。

position 方法用來設置日曆在頁面上的位置,接受left、top兩個參數。 比如說,當調用render方法初始化日曆後,我們想讓日曆顯示在頁面中間,可以這樣做

 var c = new Calender();
 c.render();
 c.position(window.innerWidth / 2, window.innerHeight / 2)

代碼執行效果如下圖

4.png | center | 826x427

hide方法用來隱藏日曆,contains方法用來檢查html元素是否包含在日曆元素之中,這兩個方法在接下來的功能實現剖析中有使用的場景。

this.onSelected = function( y, m, d ) {};
this.onClear = function() {};
this.onSetToday = function() {};

這三個方法其實並不是方法,而是事件,就像html元素的onclick事件一樣,會在特定的時候被觸發。

onSelected事件在選中日期時被觸發
onClear事件在點擊日曆右下角的「清空」按鈕時被觸發
onSetToday事件在點擊日曆右下角的「今天」按鈕時被觸發

以一個最基本的最常見的日期選着並填充文本框爲例,我們可以通過結合這三個事件和上面講解的部分方法來實現, 代碼如下

//獲得文本框元素
var dateInput = document.getElementById("date");
//實例化日曆對象
var calender = new Calender();
//綁定onSelected事件,當選中日期後被執行
calender.onSelected = function(y,m,d) {
    //填充選中的日期至文本框
    dateInput.value = [y,m,d].join("-");
    //填充後隱藏日曆
    this.hide();
}
 //綁定onSetToday事件,當點擊今天按鈕後被執行
calender.onSetToday = function() {
    var now = new Date();
    //填充當前日期至文本框
    dateInput.value = now.getFullYear() + '-' + ( now.getMonth() + 1 ) + '-' + now.getDate();
    //填充後隱藏日曆
    this.hide();
}
 //綁定onClear事件,當點擊清空按鈕後被執行
calender.onClear = function() {
    //清空文本框
    dateInput.value = "";
    //填充後隱藏日曆
    this.hide();
}
//初始化日曆
calender.render();
//因爲初始化後的日曆會顯示在頁面上,所以需要事先隱藏
calender.hide();

//但文本框獲得焦點時顯示日曆
dateInput.onfocus = function() {
    //獲得文本框在頁面中的位置,getOffset方法在之前講解過
    let offet = getOffset(this);
    //讓日曆現實在文本框的下方
    calender.position(offet.left, offet.top + 20);
}

效果如圖

5.png | center | 619x480

不知道讀者們有沒有從這段代碼中發現,日曆特效本身和輸入框之間是沒有任何關聯,它們之間的交互是通過那三個事件間接進行的,這正是軟件工程中「低耦合」設計原則的體現。在日曆和輸入框之間有一個銜接層,這個銜接層就是那三個事件, 這三個事件是可以動態設置的, 假如需求改變,我們點擊日曆時不想將值填充到文本框,而是想直接將日期發送至服務器,那麼我們只需要將onSelected事件中的代碼改爲發送數據的ajax請求代碼即可, 日曆類本身的代碼完全不用改動, 這極大的降低的代碼的維護成本。

其實這中通過事件去解耦的代碼設計方式隨處可見,比如我們點擊一個按鈕彈出一個提示消息這樣的效果

var  btn = document.getElementById("btn");
btn.onclick = function() {
    alert("hello world");
}

如上面的代碼,用的也是同樣的思路,html按鈕元素和其它JavaScript效果是沒有聯繫的,然而它必然要和外部交互,比如點擊的時候執行某個動作,否者就沒有存在的意義了。如何做到既不與外部元素綁死又能與外部元素交互?答案就是增加一個銜接層,這個銜接層就是「事件」。我們的日曆特效不正也是採用這種做法嗎。

如果瞭解設計模式的讀者應該能看的出來,這其實是策略模式的應用,如果更貼切一點也可以說是觀察者模式的應用。

接下來我們再講講Calender類內部的構建。

Calender類有6個私有的成員變量

var calendarID = newCalendarID();
var self = this;
var calendarEl;
var selectedYear;
var selectedMonth;
var selectedDate;

calendarID ,日曆html元素的ID, 調用newCalendarID方法生成, 關於此函數的細節在前文有過介紹。

self,保存Calender的this指針,供程序上下文中有需要的地方使用,因爲JavaScript中this指針不確定的原因,要在類中正確的使用this指針,必須在某個this值還指向類自身的地方將它保存下來,以供應後面的代碼使用。

calendarEl,日曆html元素的根元素。日曆是動態生成的html元素,此變量指向的就是日曆html元素的DOM對象。

selectedYear,日曆選中日期的年份

selectedMonth,日曆選中日期的月份

selectedDate,日曆選中日期的天

Calender類中除了有這六個私有變量以外,還有一系列私有方法

getStartDate
getEndDate
getContentItemHtml
getContentHtml
getCalendarHtml
getElement
genCalanderElementID
monthChangeAction
yearChangeAction
initCalendar
refreshCalender

這些方法不是Calender類對外公佈接口的一部分,但是他們參與了實現日曆的功能。 在這裏我們不一個一個的介紹方法的作用,我們根據日曆初始化代碼的執行順序來介紹他們,輪到誰就介紹誰。

日曆類被實例化後render方法首先被調用。

var calender = new Calender();
calender .render();

newCalender()實例化的過程很簡單,無非就是聲明和初始化部分成員變量的值,真正的大戲是render方法被調用。

this.render = function( placeholder ) {
       var now = new Date();
       selectedYear = now.getFullYear();
       selectedMonth = now.getMonth();
       selectedDate = now.getDate();
       initCalendar( selectedYear, selectedMonth, selectedDate, placeholder );
}

rander方法接受一個placeholder參數,這個參數是一個html元素的ID,表示日曆初始化後所在的位置,也就是說當表示日曆的html元素生成後,會成爲ID爲這個參數的值的元素的子元素,假如調用render方法時不指定這個參數, 那麼日曆html成爲body子元素。

接着,render方法會將類的三個表示選中的年月日的成員變量設置爲當前的年月日,然後在調用私有方法initCalendar初始化日曆, initCalendar承載着生成日曆的主要工作。

var initCalendar = function( placeholder ) {
    calendarEl = document.createElement( 'div' );
    calendarEl.id = calendarID;
    calendarEl.className = 'aspwebchh';
    calendarEl.innerHTML = getCalendarHtml();
    
    placeholder = placeholder ? document.getElementById( placeholder ) : document.body;
    placeholder.appendChild( calendarEl );

    refreshCalender(selectedYear, selectedMonth, selectedDate);

    monthChangeAction();
    yearChangeAction();
    dateSelectedChangeAction();
}

我們知道 calendarEl 成員變量表示日曆的html元素,在initCalendar方法中,它被初始化了。 從代碼中可一看出,它是一個div元素,被設置一個唯一id,被設置一個class, 日曆的html結構由 getCalendarHtml 方法生成, 並被設爲 id 爲placeholder的值的子元素,如果id爲placeholder的元素不存在,那麼由body元素代替它。

現在,我們來重點看看 getCalendarHtml 這個方法,日曆的html結構是由它動態生成的。

var getCalendarHtml = function() {
    var html =  '    <div class="calendar_tool" id="'+ genCalanderElementID("tool") +'">'+
                '        <div class="calendar_month">'+
                '            <select id="'+ genCalanderElementID("month_select") +'"><option value="0">1月</option>'+
                '                <option value="1">2月</option>'+
                '                <option value="2">3月</option>'+
                '                <option value="3">4月</option>'+
                '                <option value="4">5月</option>'+
                '                <option value="5">6月</option>'+
                '                <option value="6">7月</option>'+
                '                <option value="7">8月</option>'+
                '                <option value="8">9月</option>'+
                '                <option value="9">10月</option>'+
                '                <option value="10">11月</option>'+
                '                <option value="11">12月</option></select>'+
                '        </div>'+
                '        <div class="calendar_year">'+
                '            <input type="button" value="&lt;" class="calendar_year_left" id="'+ genCalanderElementID("year_prev") +'"><input'+
                '                type="text" class="calendar_year_input" id="'+ genCalanderElementID("year_input") +'"><input type="button"'+
                '                value="&gt;" class="calendar_year_right" id="'+ genCalanderElementID("year_next") +'">'+
                '        </div>'+
                '    </div>'; 

    html += '<div class="calendar_content" id="'+ genCalanderElementID("date_list") +'"></div>';

    html += '<div class="calendar_action">' +
                '<input type="button" value="清空" id="'+ genCalanderElementID("clear") +'">' +
                '<input type="button" value="今天" id="'+ genCalanderElementID("today") +'">'+
            '</div>';

    return '<div class="calendar_body">' +  html + '</div>';
}

由代碼可以看出,getCalendarHtml 方法就是通過動態拼接JavaScript字符串生成日曆的html的。在此方法中, 還是一個 genCalanderElementID  方法被頻繁的調用,這個方法的代碼如下

var genCalanderElementID = function( id ) {
    return calendarID + "_" + id;
}

他的作用就是用來生成日曆的一些子元素的ID, 當然,生成的ID全局唯一的, 因爲它的前綴就是標識日曆唯一性的calendarID。

6.png | center | 503x310

標紅的就是用這個方法生成的ID

這個時候生成的日曆html元素還並不完整,日期部分處於缺失狀態, 如下圖

7.png | center | 156x188

我們再回到處於調用棧上一層的initCalendar方法中來,當日歷的外圍html結構生成完畢以後,接着會調用 refreshCalender 方法。

refreshCalender方法的作用是刷新日曆的界面, 它接受年月日三個參數, 根據這三個參數來更新日曆的界面。

8.png | center | 159x188

9.png | center | 158x188

上面兩長圖片是分別給refreshCalender傳遞2018,5,16和2018,6,13兩組參數的執行結果,可以看出,此方法是整個日曆特效的核心方法,日曆界面的更新變化都要靠它。

refreshCalender方法做三件事請。

  1. 設置日曆界面上年份輸入框的值。
  2. 設置日曆界面上月份選擇框的值。
  3. 生成日曆界面日期部分的html。這一步是最重要的一步,通過調用 getContentHtml 來完成。

getContentHtml  方法接受年和月兩個參數,生成整一個月份的html

10.png | center | 141x149

上圖就是 getContentHtml  方法生成的內容。 方法的開頭有這樣兩行代碼用來獲得日期範圍。

var startDate = getStartDate( y, m );
var endDate = getEndDate( y, m ); 

這個日期範圍是必須的。日曆效果的一個特點是要做到星期和日期對應,望一眼日期就能知道是星期幾。此外,日曆界面還要保持工整, 因此, 我們必須要知道日曆的第一週的開始時間是幾號,日曆的最後一週結束日期是幾號, 要知道爲保持日曆界面的工整,每一頁日曆展示的日期都是需要跨月的,上面的兩行代碼就是獲得每一頁日曆的開始日期和結束日期的。

11.png | center | 159x188

以上圖爲例,一個5月份的日曆,那麼這一頁的開始日期是4月29日,週日;結束日期是6月5日,週二。

之後的代碼就是根據這個時間範圍生成html,並通過判斷日期給每個日期元素加上對應的css class屬性, 因爲我們要讓非本月份的日期顯示成灰色, 本月份的日期顯示成藍色,當前日期擁有藍色背景。具體的實現細節可以通過閱讀下面的代碼清單獲知,在這裏就不贅述了。

var getStartDate = function( y, m ) {
    var dt = new Date( y, m, 1 );
    var week = dt.getDay();
    dt.setDate( dt.getDate() - week );
    return dt;
}
        
var getEndDate = function( y, m ) {
    var dt = new Date( y, m ,1 );
    dt.setMonth( dt.getMonth() + 1 );
    dt.setDate( 0 );
    return dt;
}

var getContentItemHtml = function( date, currMonth ) {
    var content = '';
    if( date.getDate() == selectedDate && date.getMonth() == selectedMonth && date.getFullYear() == selectedYear ) {
        content += '<li class="selected">';
    } else if( currMonth == date.getMonth() ) {
        content += '<li class="c">'
    } else {
        content += '<li>';
    }
    content += '<a href="javascript:;">' + date.getDate() +'</a>';
    content += '</li>';
    return content;
}

        var getContentHtml = function( y, m ) {
    var startDate = getStartDate( y, m );
    var endDate = getEndDate( y, m );
    var title = '<dl class="calendar_title"><dd>日</dd><dd>一</dd><dd>二</dd><dd>三</dd><dd>四</dd><dd>五</dd><dd>六</dd></dl>';
    
    var content = '<ul>';
            for( var i = 0; i < 38; i++ ) {
        content += getContentItemHtml(startDate, m);
        if( ( i + 1 ) % 7 == 0 ) {
            content += '</ul><ul>';
        }
        startDate.setDate( startDate.getDate() + 1 );
    }
    content += '</ul>';
    return title + content;
        }

讓我們再回到 initCalendar 方法中來,調用  refreshCalender 方法後, 接下是

monthChangeAction();
yearChangeAction();
dateSelectedChangeAction();

這三個方法的調用。

這三個方法的作用是給日曆中的元素綁定操作效果事件的。

monthChangeAction方法用於當日歷的月份選擇的值改變時刷新日曆的日期部分內容

var monthChangeAction = function() {
    var monthSelect = getElement( 'month_select' );
    var yearInput = getElement( 'year_input' );
    
    addEventHandler( monthSelect, 'change', function() {
        var month = this.value;
        var year = yearInput.value;
        getElement( 'date_list' ).innerHTML = getContentHtml( year, month );
    } );
}

yearChangeAction方法用於當日歷的年份改變時刷新日曆的日期面板

var yearChangeAction = function() {
var monthSelect = getElement( 'month_select' );
var yearInput = getElement( 'year_input' );
var yearPrev = getElement( 'year_prev' );
var yearNext = getElement( 'year_next' );

addEventHandler( yearInput, 'blur', function() {
    if( /[^\d]+/.test( this.value ) ) {
        this.value = this.value.replace( /[^\d]+/g, '' );
    } 
    getElement( 'date_list' ).innerHTML = getContentHtml( yearInput.value, monthSelect.value ); 
} );

addEventHandler( yearPrev, 'click', function() {
    var year = yearInput.value;
    var month = monthSelect.value;
    getElement( 'date_list' ).innerHTML = getContentHtml( --year, month ); 
    yearInput.value = year;
} );

addEventHandler( yearNext, 'click', function() {
    var year = yearInput.value;
    var month = monthSelect.value;
    getElement( 'date_list' ).innerHTML = getContentHtml( ++year, month ); 
        yearInput.value = year;
    } );           
}

yearChangeAction相對monthChangeAction較爲複雜,因爲它不但要處理年份輸入框的事件, 還要處理“上一年”和 “下一年”兩個按鈕的的事件處理。

dateSelectedChangeAction用於處理日期選擇事件、“清空”按鈕事件、“今天”按鈕事件, 從方法的代碼結構中就可以看出方法的功能由這三部分構成。

var dateSelectedChangeAction = function() {
    //日期選中處理
    addEventHandler( getElement( 'date_list' ), 'click', function( e ) {
        e = e || window.event;
        var t = e.target || e.srcElement;
        if( t.tagName != 'A' ) {
            return;
        }

        var year = getElement( 'year_input' ).value;
        var month = getElement( 'month_select' ).value;
        var date = t.innerHTML;

        if( typeof( self.onSelected ) == 'function' ) {
            self.onSelected( parseInt( year ), parseInt( month ), parseInt( date ) );
        }
    } );

         //“清空”按鈕點擊處理
    addEventHandler( getElement( 'clear' ), 'click', function() {
        if( typeof( self.onClear ) == 'function' ) {
            self.onClear();
        }
    } );

        //“今天”按鈕點擊處理
    addEventHandler( getElement( 'today' ), 'click', function() {
        if( typeof( self.onSetToday ) == 'function' ) {
            self.onSetToday();
        }
    } ); 
}

這三部分事件處理代碼其實本身不執行具體的功能, 它們的真正作用是觸發另一個事件。具體一點說,這個方法做了這麼三件事情

  • 當選擇日曆的具體日期時,Calender類實例的onSelected事件被觸發
  • 當點擊日曆的“清空”按鈕的時,Calender類實例的onClear事件被觸發
  • 當點擊日曆的“今天”按鈕時,Calender類實例的onSetToday事件被觸發

這三個事件我們在前面講過是用於解除日曆本身與使用日曆的頁面的耦合的,如此能使日曆更加通用化。

至此calendar_core.js中的Calender類的內部結構已經解析完畢,一款功能完善的日曆特效呈現在了我們面前

window.Calender = Calender;

通過這行代碼導出日曆類,我們就可以在外部使用它了。

接下來我們說說如何去使用它。

使用日曆核心類 - calendar.js

通常,日曆特效的使用都會伴隨着輸入框,如下圖所示

12.png | center | 300x229

當日歷上的日期被選中時,着個日期會別填充到輸入框裏。 同時,這個日曆是但實例的,不管頁面上有多少個輸入框需要輸入日期,同一時刻,頁面上只能有一個日曆, 一個日曆服務與多個不同的文本框。

calendar.js文件中的代碼示例就是以此模式實現。

(function(){
    var single;
    var element;

    function closeHandler( e ) {
        e = e || window.event;
        var t = e.target || e.srcElement;
        if( single.contains( t ) ) {
            return;
        }
        if( t == element ) {
            return;
        }
        single.hide();
    }
    

    function checkElementValue() {
        if( !element.checkDateAction ) {
            addEventHandler( element, 'blur', function() {
                if( this.value != '' && this.value != undefined && !checkDate( this.value ) ) {
                    alert( '日期格式不正確' );
                    this.value = '';
                }
            } );
            element.checkDateAction = true;
        }
    }

    function renderCalendar() {
        if( !single ) {
            single = new Calender();
            single.onSelected = function( y, m, d ) {
                var datestr = y + '-' + ( m + 1 )  + '-' + d;
                element.value = datestr;
                this.hide();
            }
            single.onSetToday = function() {
                var now = new Date();
                element.value = now.getFullYear() + '-' + ( now.getMonth() + 1 ) + '-' + now.getDate();
                this.hide();
            }
            single.onClear = function() {
                element.value = '';
                this.hide();
            }
            single.render();
        }
        var offset = getOffset( element );
        single.position( offset.left, offset.top + element.offsetHeight );
    }

    function initCalendarSelectedValue() {
        var date = element.value;
        if( checkDate( date ) ) {
            var date    = date.replace(/\-|\/|\./g,"/");
            var ymd    = date.split("/");
            var y    = parseInt(ymd[0]);
            var m    = parseInt(ymd[1]) - 1;
            var d    = parseInt(ymd[2]);
            single.setDate( y, m, d );
        }
    }

    function calendar() {
        var e = calendar.caller.arguments[0] || window.event;
        element = e.target || e.srcElement; 
       
        removeEventHandler( document.documentElement, 'click', closeHandler );
        checkElementValue();
        renderCalendar();
        initCalendarSelectedValue();
        addEventHandler( document.documentElement, 'click', closeHandler );
    }

    window.calendar = calendar;
})();

calendar.js中有2個全局變量和5個函數

var single;
var element;
closeHandler()
checkElementValue()
renderCalendar(element)
initCalendarSelectedValue()
calendar()

變量single是日曆的實例,它只被初始化一次,可以把它看成一個單列。

變量element是調用日曆的輸入框,它會在calendar函數調用時被重複賦值,引用當前input輸入框的DOM對象。

calendar是主函數,唯一一個被導出到頁面使用的函數,其它的函數是calendar函數功能的部分,爲了使代碼易於維護纔將他們提煉成爲函數。

我們看到,其它4個函數都會在calendar函數中的某個位置出現

closeHandler 是一個工具函數, 用於實現點擊頁面上除日曆本身以外的任何位置便隱藏日曆的效果的。

checkElementValue 用於檢查文本框中默認有值的情況下值的格式是否正確,假如不正確則給予提示。

renderCalendar用於實例化日曆,並設置相應的事件,被初始化的實例是唯一的, 與此同時, 日曆將被顯示在輸入框的下方。

initCalendarSelectedValue用於將輸入框中的默認值設置爲日曆的當前日期。

最後, calendar函數被導出

window.calendar = calendar;

在頁面中使用即可,使用方式很簡潔

<input type="text" onfocus="calendar()" id="begin_time" />

觸發輸入框的focus事件即能使用日曆。

至此,一款完整的日曆的所有細節展現在了我們面前。這款日曆功能很簡單, 可它有一個優點,它的代碼結構清晰,類和函數之間,方法與方法之間,職責異常清晰。 日曆本身與頁面之間是解耦的,互相之間通過事件進行通信, 這使得日曆的代碼複用能力變強了,如果我們哪天想把這個日曆挪作他用, 只需要提取出calendar_core.js中的代碼,稍微改動即可,至於calender.js中的代碼完全可以忽視。這是高內聚低耦合軟件設計思想的體現,以被業界證明是有效的提升代碼可維護性的思想,除了日曆,也適合在任何程序設計環境中使用。所以, 這篇文章與其說是在講解日曆特效的編寫,還不如說是在講解如何設計出結構優良的代碼的方法,從某種角度來講,這比寫出炫麗的JavaScript特效更加有用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章