基于datepicker的只选择年月的日期选择器

手头上的项目在开始做的时候,有那种需要用到日期选择的需求,本着快的原则,当时选择了datepicker这一款jqueryUI的插件,当然这个插件的质量相当不错,当时的需求是要完成一个有起止时间的选择,自己的实现也是通过改变datepicker的配置项来更新相应的限制,算起来,这都是自己刚工作的时候开发的东西,那时候自己的水平大概停留一下百度一下什么插件比较合适,做一个简单的封装之类的。

这两天还是这个日期选择,有一个新的需求,不要天的选择了,只要年、月。OMG,这是个什么鬼,但是没办法啊,产品说要做,关键自己也认同这个逻辑,那就做呗。本着最省劲的原则,那就还是拿datepicker,来做一个简单的改造,事实证明,我还是too young。

首先百度了一下,只显示年月的思路,大体的关键是,

1.设置dateForamt为:"yy-mm",//只显示年月

2.设置changeYear、changeMonth为true,//开启年、月对应的下拉框,用户选择起来更方便

3.设置onChangeMonthYear的回调函数,//年月发生改变之后,就可以获取当前选中的值,拿到这个值之后,设置输入框的值

4.页面上加一个style,将选择天的pane隐藏掉。

关键点非常的简单,但是里面的坑很大,慢慢跳。

当时只是大概试了一下,拿了一个输入框做了实验,觉得好像没问题,我就想之前已经有一个dateSelect的组件了,但是写的不太通用,基本上已经把很多东西都限定死了,那这一次,趁着这次实现的机会,重写一个通用性更高的组件,说干就干。

这种起止日期的组件的关键可能就是前后的限定了,开始的日期选定了之后,结束的日期的最小值不能小于开始时间的值,反过来,如果一上来结束的日期选定了之后,那么开始的日期就不能超过这个值。这个其实实现起来比较简单,大概就是每一个输入框绑定onSelect的回调,一旦选择了之后,把另一项的maxDate或者minDate的值设定成这个值就可以了。来看一下代码:

/**
 * date_selectCom_view.js
 * 通用的日期选择组件,可自定义定制,用于组合(一开始、一结束)com-common
 * 用法
    var chooseType = 0;//先选中的类型 1-start 2-end
    var dateSelect = $().dateSelectCom({
        start:{
            ele:$("#start"),
            options:{
                dateFormat:"yy-mm-dd",
                maxDate: new Date(),
                onSelect: function(value){
                    if(chooseType != 2){
                        dateSelect.setLimit(2, value);
                        chooseType = 1;
                    }
                }
            }
        },
        end:{
            ele:$("#end"),
            options:{
                dateFormat:"yy-mm-dd",
                maxDate: new Date(),
                onSelect: function(value){
                    if(chooseType != 1){
                        dateSelect.setLimit(1, null, value);
                        chooseType = 2;
                    }
                }
            }
        }
    });

    --重置
    dateSelect.reset();
    chooseType = 0;//这个也需要重置,要不然,会出问题
 */
(function(){
    /**
     * @param params{
            start:{
                options: ,
                ele:
            }, //开始的元素
            end:{
                options: ,
                ele:
            } //结束的元素
       }
     */
    var DateSelectCom = function(params){
        if(params && params.start.ele && params.end.ele){
            var options = {dateFormat: 'yy-mm-dd'}; 
            this.startEle = $(params.start.ele);
            this.startOptions = params.start.options;
            this.endEle = $(params.end.ele);
            this.endOptions = params.end.options;
            this.init();//初始化
        }
    };

    /**
     * 日期的处理,低版本的ie对Date对象的兼容较差
     */
    DateSelectCom.prototype.dateHandle = function(d){
        if(d){
            if(d.replace){
                var t;
                if(d.indexOf("-") >= 0 && d.split("-").length == 2){//-分割,并且只有年月,补上最后一位, 兼容IE
                    d += "-1";
                }
                else if(d.indexOf("/") >= 0 && d.split("/").length ==2){// /分割
                    d += "/1";
                }
                t = d.replace(/-/g,'/');
                return new Date(t);
            }
            return new Date(d);
        }
        return '';
    };

    /**
     * 设置限制
     * @param type 1-start 2-end
     * @param min 最小值
     * @param max 最大值
     */
    DateSelectCom.prototype.setLimit = function(type, min, max){
        if(type){
            var minDate, maxDate, eleList = ["start", "end"],
                ele = this[eleList[type-1]+"Ele"],
                options = this[eleList[type-1]+"Options"];
            if(min){
                minDate = this.dateHandle(min);
                ele.datepicker("option", "minDate", minDate);
            }
            else if(max){
                if(!options.maxDate || (this.dateHandle(max)) < (this.dateHandle(options.maxDate))){
                    maxDate = this.dateHandle(max);
                    ele.datepicker("option", "maxDate", maxDate);
                }
            }
        }
    };

    /**
     * 设置值
     * @param type 1-start 2-end
     * @param value 值
     */
    DateSelectCom.prototype.setValue = function(type, value){
        var eleList = ["start", "end"],
            ele = this[eleList[type-1]+"Ele"];
        if(ele){
            ele.datepicker("setDate", this.dateHandle(value));
        }
    };

    /**
     * 获取值
     * @param type 1-start 2-end
     */
    DateSelectCom.prototype.getValue = function(type){
        var eleList = ["start", "end"],
            ele = this[eleList[type-1]+"Ele"];
        if(ele){
            return ele.val();
        }
    };

    /**
     * 设置默认值
     * @param type 1-start 2-end
     * @param val 默认值
     */
    DateSelectCom.prototype.setDefault = function(type, val){
        var eleList = ["start", "end"],
            ele = this[eleList[type-1]+"Ele"];
        if(ele){
            ele.datepicker("option", "defaultDate", this.dateHandle(val));
        }
    };

    /**
     * 开始的初始化
     */
    DateSelectCom.prototype.startInit = function(){
        this.startEle.datepicker("destroy");
        this.startEle.datepicker(this.startOptions);
    };

    /**
     * 结束的初始化
     */
    DateSelectCom.prototype.endInit = function(){
        this.endEle.datepicker("destroy");
        this.endEle.datepicker(this.endOptions);
    };

    /**
     * 重置
     */
    DateSelectCom.prototype.reset = function(){
        this.startEle.val("");//清掉数据
        this.endEle.val("");
        this.startInit();
        this.endInit();
    };

    /**
     * 初始化
     */
    DateSelectCom.prototype.init = function(){
        this.startInit();
        this.endInit();
    };

    $.fn.extend({dateSelectCom:function(params){
        return new DateSelectCom(params);
    }});
})();

因为是一个通用化的组件,所以只是封装了一个设置限定的函数,另外onSelect的绑定也没有集成在组件里面,而是通过初始化的时候传参的形式传进去,我觉得这样的自由度会更高一点。另外,这是我最后完成的版本,所以里面有一些之前没有提到的工具函数,这个会在后面提到,大家可以先忽略。

好啦,高潮来了,组件完成了之后,测试了一下,类似于住市里面用法的那段代码,就是正常的年月日的选择,ok,正常工作,完全可以拿来替代之前写的那个组件,那这次要实现的只显示年月的功能呢,把dateFormat设置为“yy-mm”,好像也没有什么特别的问题,哎,等一下,好像不对。我打开日期选择框,切换了年月之后,输入框的值变掉了。为什么我再次打开这个日期选择器的时候,输入框的值是对的,可是下面的日期是今天的日期?

我再说的具体一点,我打开日期选择器,选择2015-01,输入框变成2015-01,选择器关闭。再次打开日期选择器,输入框还是2015-01,但是日期选择器显示选定的时间是2015-09(当前的日期),这很明显是不对的,是不是我在onChangeMonthYear的回调函数里面设置输入框的值的方法不对呢,只是设定了value,但是没有触发datepicker内部的更新。这个是有可能的,于是在value的设置之后,我增加了一些事件(change、blur、keyup)的触发,但是没什么用。查看了datepicker的源码之后,发现了setDate的接口方法,于是把设定value的这部分逻辑,换成了调用setDate接口,就是调用上边我封装的组件中的setValue的方法,换成了这个之后发现还是不对,那就只能从datepicker的源码上边去找原因了。

1.setDate到底有没有成功

setDate调用的是内部的_setDateDatepicker方法。这个函数的内部实现如下,

        /* Set the dates for a jQuery selection.
	 * @param  target element - the target input field or division or span
	 * @param  date	Date - the new date
	 */
	_setDateDatepicker: function(target, date) {
		var inst = this._getInst(target);
		if (inst) {
                        this._setDate(inst, date);
			this._updateDatepicker(inst);
			this._updateAlternate(inst);
		}
	},

这里面的inst是datepicker内部维护的数据模型,当前选中的日期以及日期的格式等等信息都会存放在里面。从函数名上我们可以看到里面的逻辑大概是,更新数据、更新组件(渲染)以及更新属性。我在这个函数的最console出来了inst的值,(因为inst指向的是一个数据对象,所以它是动态的,可以在最后获取最新的值),发现我们设定的日期已经成功更新到数据模型上了,因此我们可以认定setDate是成功执行了,符合我们的期待。

2.第二次显示日期选择器的时候,原来的数据为什么失效了。

这里调用的是,datepicker的_showDatepicker函数,还是先放一下函数内部的实现,函数里面的console为调试使用,

        /* Pop-up the date picker for a given input field.
	 * If false returned from beforeShow event handler do not show.
	 * @param  input  element - the input field attached to the date picker or
	 *					event - if triggered by focus
	 */
	_showDatepicker: function(input) {
		input = input.target || input;
		console.log("!!!!!!!", input, input.value);
		if (input.nodeName.toLowerCase() !== "input") { // find from button/image trigger
			input = $("input", input.parentNode)[0];
		}

		if ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput === input) { // already here
			return;
		}

		var inst, beforeShow, beforeShowSettings, isFixed,
			offset, showAnim, duration;

		inst = $.datepicker._getInst(input);
		console.log(2, inst, inst.input, inst.input.val());
		console.log(3, inst.currentDay, inst.currentMonth, inst.currentYear);
		console.log(4, inst.selectDay, inst.selectMonth, inst.selectYear);
		if ($.datepicker._curInst && $.datepicker._curInst !== inst) {
			$.datepicker._curInst.dpDiv.stop(true, true);
			if ( inst && $.datepicker._datepickerShowing ) {
				$.datepicker._hideDatepicker( $.datepicker._curInst.input[0] );
			}
		}

		beforeShow = $.datepicker._get(inst, "beforeShow");
		beforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {};
		if(beforeShowSettings === false){
			return;
		}
		datepicker_extendRemove(inst.settings, beforeShowSettings);

		console.log(8, inst, inst.input, inst.input.val());
		console.log(9, inst.currentDay, inst.currentMonth, inst.currentYear);
		console.log(10, inst.selectDay, inst.selectMonth, inst.selectYear);

		inst.lastVal = null;
		$.datepicker._lastInput = input;
		$.datepicker._setDateFromField(inst);

		console.log(11, inst, inst.input, inst.input.val());
		console.log(12, inst.currentDay, inst.currentMonth, inst.currentYear);
		console.log(13, inst.selectDay, inst.selectMonth, inst.selectYear);

		if ($.datepicker._inDialog) { // hide cursor
			input.value = "";
		}
		if (!$.datepicker._pos) { // position below input
			$.datepicker._pos = $.datepicker._findPos(input);
			$.datepicker._pos[1] += input.offsetHeight; // add the height
		}

		isFixed = false;
		$(input).parents().each(function() {
			isFixed |= $(this).css("position") === "fixed";
			return !isFixed;
		});

		offset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]};
		$.datepicker._pos = null;
		//to avoid flashes on Firefox
		inst.dpDiv.empty();
		// determine sizing offscreen
		inst.dpDiv.css({position: "absolute", display: "block", top: "-1000px"});
		console.log(5, inst);
		console.log(6, inst.currentDay, inst.currentMonth, inst.currentYear);
		console.log(7, inst.selectDay, inst.selectMonth, inst.selectYear);
		$.datepicker._updateDatepicker(inst);
		// fix width for dynamic number of date pickers
		// and adjust position before showing
		offset = $.datepicker._checkOffset(inst, offset, isFixed);
		inst.dpDiv.css({position: ($.datepicker._inDialog && $.blockUI ?
			"static" : (isFixed ? "fixed" : "absolute")), display: "none",
			left: offset.left + "px", top: offset.top + "px"});

		if (!inst.inline) {
			showAnim = $.datepicker._get(inst, "showAnim");
			duration = $.datepicker._get(inst, "duration");
			inst.dpDiv.css( "z-index", datepicker_getZindex( $( input ) ) + 1 );
			$.datepicker._datepickerShowing = true;

			if ( $.effects && $.effects.effect[ showAnim ] ) {
				inst.dpDiv.show(showAnim, $.datepicker._get(inst, "showOptions"), duration);
			} else {
				inst.dpDiv[showAnim || "show"](showAnim ? duration : null);
			}

			if ( $.datepicker._shouldFocusInput( inst ) ) {
				inst.input.focus();
			}

			$.datepicker._curInst = inst;
		}
	},

后来发现,在我console的10、11之间inst的值发生了变化,10之前的inst的值为我们预期的值,11之后inst的值就变成了当前的日期。10、11直接唯一有嫌疑的就是这行代码了。

$.datepicker._setDateFromField(inst);
我们来看一下这个函数的内部实现。

        /* Parse existing date and initialise date picker. */
	_setDateFromField: function(inst, noDefault) {
		if (inst.input.val() === inst.lastVal) {
			return;
		}
		console.log(inst.input, inst.input.val());
		var dateFormat = this._get(inst, "dateFormat"),
			dates = inst.lastVal = inst.input ? inst.input.val() : null,
			defaultDate = this._getDefaultDate(inst),
			date = defaultDate,
			settings = this._getFormatConfig(inst);

			console.log(dateFormat, dates, settings);
			console.log("~~~~~~~",defaultDate);
		try {
			date = this.parseDate(dateFormat, dates, settings) || defaultDate;
			console.log(date, 1);
		} catch (event) {
			dates = (noDefault ? "" : dates);
			console.log(dates, 2);
		}
		console.log(date, dates);
		inst.selectedDay = date.getDate();
		inst.drawMonth = inst.selectedMonth = date.getMonth();
		inst.drawYear = inst.selectedYear = date.getFullYear();
		inst.currentDay = (dates ? date.getDate() : 0);
		inst.currentMonth = (dates ? date.getMonth() : 0);
		inst.currentYear = (dates ? date.getFullYear() : 0);
		console.log(14, inst.currentDay, inst.currentMonth, inst.currentYear, inst.selectedYear, inst.selectedMonth);
		this._adjustInstDate(inst);
		console.log(14-1, inst.currentDay, inst.currentMonth, inst.currentYear, inst.selectedYear, inst.selectedMonth);
	},

这个函数结束之后,inst的值就变掉了,我们在函数的最后可以看到inst的赋值,而这个值的来源就是date这个变量,我们看一下try,catch那段逻辑,date来源于parseDate的返回值。如果没有返回,就是默认值,默认值如果我们不设定的话,就是当前的时间。真相估计就是因为这个了,为了确定最后的犯人,我们看一下parseDate的实现,代码很长,基本上是一个日期的解析,用的是传进去的dateFormat和date按照固定的规则解析出来。只看一下这个函数的最后几行。

date = this._daylightSavingAdjust(new Date(year, month - 1, day));

if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) {
	throw "Invalid date"; // E.g. 31/02/00
}
return date;
这里面year,month,day是根据传进去的值解析出来的时间,我们再回到前面,传进这个解析函数的值是什么,
dates = inst.lastVal = inst.input ? inst.input.val() : null,
是当前输入框的value,对于之前我举的例子的话,就是2015-04,在解析函数里面解析出来的year是2015,month是4,day是-1,是的,你没看错day=-1,造成这个结果的原因有两个,第一个day默认值给的是-1,第二个,传进去的dateFormat是“yy-mm”,所以day没有解析,直接是默认值。就是因为day=-1,所以解析函数最后的那个判断就过不了,解析函数会直接抛出一个错误,inst的值就直接会变成默认值,一切都说通了。

那么,怎么去修正这个问题呢,我最开始的想法是,直接动源码,在里面加一个容错,解析函数里面我摘的代码之前,如果day=-1,直接变为1,做一个兼容,试了一下没什么问题,但是datepicker毕竟是别人写的插件,我不知道我这样的感动会不会有别的影响,而且我们网站上已经有大量页面用到这个插件,万一改坏了那就糟了,所以我没敢动,事实证明,幸亏我没动,这个坑比我想的深多了。

不动源码的情况下,怎么解决这个问题呢,一筹莫展的时候,我看到了这个jQuery UI Datepicker doesn't correctly default on mm/yy,里面提到datepicker对这种只有年月的支持有点问题,事实上我觉得datepicker的兼容已经写得很不错了,毕竟这种只有年月的情况比较少见,这种情况的兼容稍微差点,也可以理解,里面有人提出了一个解决的方案。

$('#asdf').datepicker({ changeMonth: true, changeYear: true, dateFormat: 'mm/yy',beforeShow: function(input, inst) 
{   //getter
    var defaultDate = $( "#asdf" ).datepicker( "option", "defaultDate" );
    //setter
    $( "#asdf" ).datepicker( "option", "defaultDate",   Date.parseExact($("#asdf").val(), "M/yyyy"));
}  });
这里面的关键就是设定beforeShow函数,这个函数会在日期选择器显示之前调用执行。再回到这个错误的原因上来,_setDateFromField函数内部更新了inst,如果这个输入框的value解析不出来就更新成dafaultValue,那么我们可以在日期选择器打开之前,把value的值变成defaultDate,那么value即使解析失败,inst的值,也是我们想要的值。思路就是这么个思路,可以说这就是这个问题的关键了。

本来,问题到这已经解决了大半了,但是在后面的调试中,发现涉及到datepicker更新option的时候可能都会触发onChangeMonthYear事件,也会导致一系列的问题,而且我还发现除了日期解析的那个函数以外,_setDate对传入空date的情况,会把date转换成当前的时间,触发onChangeMonthYear,但是又会把对应输入框的value清空,这就会导致开始的日期明明是空的,结束的日期却有最小值的限制,所以我为什么说这个坑很深。

上一下最后完成的代码

var chooseType = 0;//先选中的类型 1-start 2-end
var dateSelect = $().dateSelectCom({
	start:{
		ele:$("#starttime"),
		options:{
			dateFormat: 'yy-mm',
			changeMonth: true, 
			changeYear: true,
			monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],
			maxDate: new Date(),
			onChangeMonthYear: function(year, month){
				var value = year+"-"+month;
				var oldValue = dateSelect.getValue(2);
				dateSelect.setValue(1, value);
				if(chooseType != 2){
					chooseType = 1;
					dateSelect.setLimit(2, value);
					dateSelect.setValue(2, oldValue);
				}
			},
			beforeShow: function(input, inst) 
			{
				var value = dateSelect.getValue(1);
				var oldValue = dateSelect.getValue(2);
			    if(value){
				    dateSelect.setDefault(1, value);
				    dateSelect.setValue(1, value);
				    oldValue && dateSelect.setValue(2, oldValue);
				}else{
					oldValue && dateSelect.setValue(2, oldValue);//如果另一项有值,以另一项为准
					dateSelect.setValue(1, new Date());	
				}
			} 
		}
	},
	end:{
		ele:$("#endtime"),
		options:{
			dateFormat: 'yy-mm',
			changeMonth: true, 
			changeYear: true,
			monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],
			maxDate: new Date(),
			onChangeMonthYear: function(year, month){
				var value = year+"-"+month;
				var oldValue = dateSelect.getValue(1);
				dateSelect.setValue(2, value);
				if(chooseType != 1){
					chooseType = 2;
					dateSelect.setLimit(1, null, value);
					dateSelect.setValue(1, oldValue);
				}
			},
			beforeShow: function(input, inst) 
			{   
				var value = dateSelect.getValue(2);
				var oldValue = dateSelect.getValue(1);
			    if(value){
				    dateSelect.setDefault(2, value);
				    dateSelect.setValue(2, value);
				    oldValue && dateSelect.setValue(1, oldValue);
				}
				else{
					oldValue && dateSelect.setValue(1, oldValue);//如果另一项有值,以另一项为准
					dateSelect.setValue(2, new Date());
				}
			} 
		}
	}
});
至此,折腾了两三天,总算把这个完成了,感想的话有以下几点:

1.datepicker写的真不错,通用化的程度非常高,考虑的情况很多。

2.自己调试代码的能力应该是有提升的,要放在以前,估计会是两眼一抹黑的情况。

3.知道了原理去反推结果非常的方便,在做的过程中陆续发现了很多问题,因为原理通了,就很好改了。

4.自己在遇到那种比较深的逻辑的情况,依然是很艰难,内存不够,堆栈不够深,还需要继续努力啊。

你可能感兴趣的:(学习,js)