diff --git a/src/main/resources/static/js/bootstrap-datepicker.js b/src/main/resources/static/js/bootstrap-datepicker.js index f3726c5..2c5b91b 100644 --- a/src/main/resources/static/js/bootstrap-datepicker.js +++ b/src/main/resources/static/js/bootstrap-datepicker.js @@ -1,2030 +1,2136 @@ /*! - * Datepicker for Bootstrap v1.7.0 (https://github.com/uxsolutions/bootstrap-datepicker) - * - * Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * Datepicker for Bootstrap v1.7.0 (日期选择器插件) + * 基于 Apache 基于 Apache 许可证开源,用于在 Bootstrap 框架中提供日期选择功能 */ -(function(factory){ +// UMD 模块定义:兼容 AMD/CommonJS/全局变量三种模块规范 +(function(factory) { + // AMD 规范(如 RequireJS):通过 define 定义模块 if (typeof define === "function" && define.amd) { define(["jquery"], factory); - } else if (typeof exports === 'object') { + } + // CommonJS 规范(如 Node.js):通过 exports 导出模块 + else if (typeof exports === 'object') { factory(require('jquery')); - } else { + } + // 全局变量模式:直接挂载到 window.jQuery 上 + else { factory(jQuery); } -}(function($, undefined){ - function UTCDate(){ - return new Date(Date.UTC.apply(Date, arguments)); - } - function UTCToday(){ - var today = new Date(); - return UTCDate(today.getFullYear(), today.getMonth(), today.getDate()); - } - function isUTCEquals(date1, date2) { - return ( - date1.getUTCFullYear() === date2.getUTCFullYear() && - date1.getUTCMonth() === date2.getUTCMonth() && - date1.getUTCDate() === date2.getUTCDate() - ); - } - function alias(method, deprecationMsg){ - return function(){ - if (deprecationMsg !== undefined) { - $.fn.datepicker.deprecated(deprecationMsg); - } - - return this[method].apply(this, arguments); - }; - } - function isValidDate(d) { - return d && !isNaN(d.getTime()); - } - - var DateArray = (function(){ - var extras = { - get: function(i){ - return this.slice(i)[0]; - }, - contains: function(d){ - // Array.indexOf is not cross-browser; - // $.inArray doesn't work with Dates - var val = d && d.valueOf(); - for (var i=0, l=this.length; i < l; i++) - // Use date arithmetic to allow dates with different times to match - if (0 <= this[i].valueOf() - val && this[i].valueOf() - val < 1000*60*60*24) - return i; - return -1; - }, - remove: function(i){ - this.splice(i,1); - }, - replace: function(new_array){ - if (!new_array) - return; - if (!$.isArray(new_array)) - new_array = [new_array]; - this.clear(); - this.push.apply(this, new_array); - }, - clear: function(){ - this.length = 0; - }, - copy: function(){ - var a = new DateArray(); - a.replace(this); - return a; - } - }; - - return function(){ - var a = []; - a.push.apply(a, arguments); - $.extend(a, extras); - return a; - }; - })(); - - - // Picker object - - var Datepicker = function(element, options){ - $.data(element, 'datepicker', this); - this._process_options(options); - - this.dates = new DateArray(); - this.viewDate = this.o.defaultViewDate; - this.focusDate = null; - - this.element = $(element); - this.isInput = this.element.is('input'); - this.inputField = this.isInput ? this.element : this.element.find('input'); - this.component = this.element.hasClass('date') ? this.element.find('.add-on, .input-group-addon, .btn') : false; - if (this.component && this.component.length === 0) - this.component = false; - this.isInline = !this.component && this.element.is('div'); - - this.picker = $(DPGlobal.template); - - // Checking templates and inserting - if (this._check_template(this.o.templates.leftArrow)) { - this.picker.find('.prev').html(this.o.templates.leftArrow); - } - - if (this._check_template(this.o.templates.rightArrow)) { - this.picker.find('.next').html(this.o.templates.rightArrow); - } - - this._buildEvents(); - this._attachEvents(); - - if (this.isInline){ - this.picker.addClass('datepicker-inline').appendTo(this.element); - } - else { - this.picker.addClass('datepicker-dropdown dropdown-menu'); - } - - if (this.o.rtl){ - this.picker.addClass('datepicker-rtl'); - } - - if (this.o.calendarWeeks) { - this.picker.find('.datepicker-days .datepicker-switch, thead .datepicker-title, tfoot .today, tfoot .clear') - .attr('colspan', function(i, val){ - return Number(val) + 1; - }); - } - - this._process_options({ - startDate: this._o.startDate, - endDate: this._o.endDate, - daysOfWeekDisabled: this.o.daysOfWeekDisabled, - daysOfWeekHighlighted: this.o.daysOfWeekHighlighted, - datesDisabled: this.o.datesDisabled - }); - - this._allow_update = false; - this.setViewMode(this.o.startView); - this._allow_update = true; - - this.fillDow(); - this.fillMonths(); - - this.update(); - - if (this.isInline){ - this.show(); - } - }; - - Datepicker.prototype = { - constructor: Datepicker, - - _resolveViewName: function(view){ - $.each(DPGlobal.viewModes, function(i, viewMode){ - if (view === i || $.inArray(view, viewMode.names) !== -1){ - view = i; - return false; - } - }); - - return view; - }, - - _resolveDaysOfWeek: function(daysOfWeek){ - if (!$.isArray(daysOfWeek)) - daysOfWeek = daysOfWeek.split(/[,\s]*/); - return $.map(daysOfWeek, Number); - }, - - _check_template: function(tmp){ - try { - // If empty - if (tmp === undefined || tmp === "") { - return false; - } - // If no html, everything ok - if ((tmp.match(/[<>]/g) || []).length <= 0) { - return true; - } - // Checking if html is fine - var jDom = $(tmp); - return jDom.length > 0; - } - catch (ex) { - return false; - } - }, - - _process_options: function(opts){ - // Store raw options for reference - this._o = $.extend({}, this._o, opts); - // Processed options - var o = this.o = $.extend({}, this._o); - - // Check if "de-DE" style date is available, if not language should - // fallback to 2 letter code eg "de" - var lang = o.language; - if (!dates[lang]){ - lang = lang.split('-')[0]; - if (!dates[lang]) - lang = defaults.language; - } - o.language = lang; - - // Retrieve view index from any aliases - o.startView = this._resolveViewName(o.startView); - o.minViewMode = this._resolveViewName(o.minViewMode); - o.maxViewMode = this._resolveViewName(o.maxViewMode); - - // Check view is between min and max - o.startView = Math.max(this.o.minViewMode, Math.min(this.o.maxViewMode, o.startView)); - - // true, false, or Number > 0 - if (o.multidate !== true){ - o.multidate = Number(o.multidate) || false; - if (o.multidate !== false) - o.multidate = Math.max(0, o.multidate); - } - o.multidateSeparator = String(o.multidateSeparator); - - o.weekStart %= 7; - o.weekEnd = (o.weekStart + 6) % 7; - - var format = DPGlobal.parseFormat(o.format); - if (o.startDate !== -Infinity){ - if (!!o.startDate){ - if (o.startDate instanceof Date) - o.startDate = this._local_to_utc(this._zero_time(o.startDate)); - else - o.startDate = DPGlobal.parseDate(o.startDate, format, o.language, o.assumeNearbyYear); - } - else { - o.startDate = -Infinity; - } - } - if (o.endDate !== Infinity){ - if (!!o.endDate){ - if (o.endDate instanceof Date) - o.endDate = this._local_to_utc(this._zero_time(o.endDate)); - else - o.endDate = DPGlobal.parseDate(o.endDate, format, o.language, o.assumeNearbyYear); - } - else { - o.endDate = Infinity; - } - } - - o.daysOfWeekDisabled = this._resolveDaysOfWeek(o.daysOfWeekDisabled||[]); - o.daysOfWeekHighlighted = this._resolveDaysOfWeek(o.daysOfWeekHighlighted||[]); - - o.datesDisabled = o.datesDisabled||[]; - if (!$.isArray(o.datesDisabled)) { - o.datesDisabled = o.datesDisabled.split(','); - } - o.datesDisabled = $.map(o.datesDisabled, function(d){ - return DPGlobal.parseDate(d, format, o.language, o.assumeNearbyYear); - }); - - var plc = String(o.orientation).toLowerCase().split(/\s+/g), - _plc = o.orientation.toLowerCase(); - plc = $.grep(plc, function(word){ - return /^auto|left|right|top|bottom$/.test(word); - }); - o.orientation = {x: 'auto', y: 'auto'}; - if (!_plc || _plc === 'auto') - ; // no action - else if (plc.length === 1){ - switch (plc[0]){ - case 'top': - case 'bottom': - o.orientation.y = plc[0]; - break; - case 'left': - case 'right': - o.orientation.x = plc[0]; - break; - } - } - else { - _plc = $.grep(plc, function(word){ - return /^left|right$/.test(word); - }); - o.orientation.x = _plc[0] || 'auto'; - - _plc = $.grep(plc, function(word){ - return /^top|bottom$/.test(word); - }); - o.orientation.y = _plc[0] || 'auto'; - } - if (o.defaultViewDate instanceof Date || typeof o.defaultViewDate === 'string') { - o.defaultViewDate = DPGlobal.parseDate(o.defaultViewDate, format, o.language, o.assumeNearbyYear); - } else if (o.defaultViewDate) { - var year = o.defaultViewDate.year || new Date().getFullYear(); - var month = o.defaultViewDate.month || 0; - var day = o.defaultViewDate.day || 1; - o.defaultViewDate = UTCDate(year, month, day); - } else { - o.defaultViewDate = UTCToday(); - } - }, - _events: [], - _secondaryEvents: [], - _applyEvents: function(evs){ - for (var i=0, el, ch, ev; i < evs.length; i++){ - el = evs[i][0]; - if (evs[i].length === 2){ - ch = undefined; - ev = evs[i][1]; - } else if (evs[i].length === 3){ - ch = evs[i][1]; - ev = evs[i][2]; - } - el.on(ev, ch); - } - }, - _unapplyEvents: function(evs){ - for (var i=0, el, ev, ch; i < evs.length; i++){ - el = evs[i][0]; - if (evs[i].length === 2){ - ch = undefined; - ev = evs[i][1]; - } else if (evs[i].length === 3){ - ch = evs[i][1]; - ev = evs[i][2]; - } - el.off(ev, ch); - } - }, - _buildEvents: function(){ +}(function($, undefined) { // 传入 jQuery 并声明 undefined 避免被篡改 + + // ------------------------------ + // 工具函数:日期处理与通用逻辑 + // ------------------------------ + + /** + * 创建 UTC 日期(避免时区差异导致的日期计算错误) + * 例如:UTCDate(2024, 5, 1) → 2024年6月1日 00:00:00 UTC + * @returns {Date} UTC 日期对象(时间部分强制为 00:00:00) + */ + function UTCDate() { + // 使用 Date.UTC 构建 UTC 时间,再转为 Date 对象 + return new Date(Date.UTC.apply(Date, arguments)); + } + + /** + * 获取当天的 UTC 日期(时间部分归零) + * 解决本地时区转换问题,例如北京时间 20:00 对应的 UTC 是 12:00,需统一为当天 + * @returns {Date} 当天 UTC 日期 + */ + function UTCToday() { + var today = new Date(); // 本地当前时间 + // 提取年月日构建 UTC 日期(忽略时间部分) + return UTCDate(today.getFullYear(), today.getMonth(), today.getDate()); + } + + /** + * 判断两个日期是否为同一天(忽略时间和时区差异) + * @param {Date} date1 日期1 + * @param {Date} date2 日期2 + * @returns {boolean} 是否为同一天 + */ + function isUTCEquals(date1, date2) { + if (!date1 || !date2) return false; // 处理无效日期 + return ( + date1.getUTCFullYear() === date2.getUTCFullYear() && // 年相等 + date1.getUTCMonth() === date2.getUTCMonth() && // 月相等 + date1.getUTCDate() === date2.getUTCDate() // 日相等 + ); + } + + /** + * 为方法创建别名(支持弃用警告) + * 用于版本兼容,当旧方法被调用时提示警告并转发到新方法 + * @param {string} method 原方法名 + * @param {string} deprecationMsg 警告信息 + * @returns {Function} 包装后的方法 + */ + function alias(method, deprecationMsg) { + return function() { + if (deprecationMsg) $.fn.datepicker.deprecated(deprecationMsg); + return this[method].apply(this, arguments); // 转发调用 + }; + } + + /** + * 判断是否为有效日期 + * @param {Date} d 日期对象 + * @returns {boolean} 是否有效(排除 Invalid Date) + */ + function isValidDate(d) { + return d && !isNaN(d.getTime()); // 无效日期的 getTime() 返回 NaN + } + + + // ------------------------------ + // DateArray:增强型日期数组(管理选中日期) + // ------------------------------ + + var DateArray = (function() { + // 扩展数组的方法 + var extras = { + /** + * 按索引获取日期(支持负数,如 -1 取最后一个) + * @param {number} i 索引 + * @returns {Date|undefined} 日期 + */ + get: function(i) { + // 处理负数索引:slice(-1) 取最后一个元素 + return this.slice(i)[0]; + }, + + /** + * 判断数组是否包含指定日期(忽略时间,仅比较年月日) + * @param {Date} d 日期 + * @returns {number} 索引(-1 表示不包含) + */ + contains: function(d) { + if (!d) return -1; + var val = d.valueOf(); // 转为时间戳 + for (var i = 0, l = this.length; i < l; i++) { + // 允许时间不同但年月日相同(差值 < 24小时) + var diff = this[i].valueOf() - val; + if (0 <= diff && diff < 1000 * 60 * 60 * 24) + return i; + } + return -1; + }, + + /** + * 删除指定索引的日期 + * @param {number} i 索引 + */ + remove: function(i) { + this.splice(i, 1); // 从索引 i 开始删除 1 个元素 + }, + + /** + * 替换数组内容 + * @param {Date|Date[]} new_array 新日期或日期数组 + */ + replace: function(new_array) { + if (!new_array) return; + if (!$.isArray(new_array)) new_array = [new_array]; // 统一为数组 + this.clear(); // 清空现有内容 + this.push.apply(this, new_array); // 添加新内容 + }, + + /** + * 清空数组 + */ + clear: function() { + this.length = 0; // 直接重置长度 + }, + + /** + * 复制数组(深拷贝日期对象) + * @returns {DateArray} 新数组 + */ + copy: function() { + var a = new DateArray(); + a.replace(this); // 逐个复制日期 + return a; + } + }; + + // 构造函数:创建增强数组 + return function() { + var a = []; // 基础数组 + a.push.apply(a, arguments); // 初始化元素 + $.extend(a, extras); // 扩展方法 + return a; + }; + })(); + + + // ------------------------------ + // Datepicker 核心类:日期选择器主体 + // ------------------------------ + + var Datepicker = function(element, options) { + // 将实例绑定到 DOM 元素(方便通过 $(el).data('datepicker') 获取) + $.data(element, 'datepicker', this); + this._process_options(options); // 处理配置项 + + // 初始化核心属性 + this.dates = new DateArray(); // 选中的日期集合(支持多日期) + this.viewDate = this.o.defaultViewDate; // 当前视图显示的日期(如“2024-06”) + this.focusDate = null; // 键盘导航时的聚焦日期 + + // 绑定 DOM 元素 + this.element = $(element); + this.isInput = this.element.is('input'); // 是否为单个输入框 + // 实际输入框:如果是输入框则自身,否则查找内部 input + this.inputField = this.isInput ? this.element : this.element.find('input'); + // 带按钮的组件(如 input-group 中的图标按钮) + this.component = this.element.hasClass('date') ? + this.element.find('.add-on, .input-group-addon, .btn') : false; + this.isInline = !this.component && this.element.is('div'); // 是否为内联日历(直接显示在页面中) + + // 创建日历 DOM 结构(基于全局模板) + this.picker = $(DPGlobal.template); + + // 替换左右箭头模板(支持自定义图标,如 FontAwesome) + if (this._check_template(this.o.templates.leftArrow)) { + this.picker.find('.prev').html(this.o.templates.leftArrow); + } + if (this._check_template(this.o.templates.rightArrow)) { + this.picker.find('.next').html(this.o.templates.rightArrow); + } + + // 绑定事件(输入框、按钮、日历内部事件) + this._buildEvents(); + this._attachEvents(); + + // 处理内联/下拉样式 + if (this.isInline) { + this.picker.addClass('datepicker-inline').appendTo(this.element); // 内联直接添加到容器 + } else { + this.picker.addClass('datepicker-dropdown dropdown-menu'); // 下拉样式 + } + + // 处理 RTL(从右到左)布局(如阿拉伯语) + if (this.o.rtl) { + this.picker.addClass('datepicker-rtl'); + } + + // 显示周数时调整列数(默认 7 列,加 1 列周数) + if (this.o.calendarWeeks) { + this.picker.find('.datepicker-days .datepicker-switch, thead .datepicker-title, tfoot .today, tfoot .clear') + .attr('colspan', function(i, val) { + return Number(val) + 1; // 原 colspan 加 1 + }); + } + + // 重新处理日期范围配置(确保格式正确) + this._process_options({ + startDate: this._o.startDate, + endDate: this._o.endDate, + daysOfWeekDisabled: this.o.daysOfWeekDisabled, + daysOfWeekHighlighted: this.o.daysOfWeekHighlighted, + datesDisabled: this.o.datesDisabled + }); + + // 初始化视图模式(限制在 minViewMode 和 maxViewMode 之间) + this._allow_update = false; // 临时禁用更新 + this.setViewMode(this.o.startView); + this._allow_update = true; + + // 渲染星期标题(Su, Mo, ...)和月份列表(Jan, Feb, ...) + this.fillDow(); + this.fillMonths(); + + // 更新日历内容(渲染日期单元格) + this.update(); + + // 内联日历默认显示 + if (this.isInline) { + this.show(); + } + }; + + Datepicker.prototype = { + constructor: Datepicker, + + /** + * 解析视图模式名称为索引(统一格式) + * 视图模式层级:days(0) → months(1) → years(2) → decades(3) → centuries(4) + * @param {string|number} view 视图名称(如 'days')或索引(如 0) + * @returns {number} 视图索引 + */ + _resolveViewName: function(view) { + $.each(DPGlobal.viewModes, function(i, viewMode) { + // 匹配索引或名称(如 0 或 'days' 都返回 0) + if (view === i || $.inArray(view, viewMode.names) !== -1) { + view = i; + return false; // 退出循环 + } + }); + return view; + }, + + /** + * 解析星期配置(将字符串/数组转为数字数组) + * 星期对应:0=周日,1=周一,...,6=周六 + * @param {string|number[]} daysOfWeek 星期配置(如 "0,6" 或 [0,6]) + * @returns {number[]} 数字数组 + */ + _resolveDaysOfWeek: function(daysOfWeek) { + if (!$.isArray(daysOfWeek)) + daysOfWeek = daysOfWeek.split(/[,\s]*/); // 字符串转数组(支持逗号/空格分隔) + return $.map(daysOfWeek, Number); // 转为数字 + }, + + /** + * 检查模板是否有效(用于箭头图标) + * @param {string} tmp 模板字符串(如 '') + * @returns {boolean} 是否有效 + */ + _check_template: function(tmp) { + try { + if (tmp === undefined || tmp === "") return false; + // 无标签的纯文本(如 '<<')直接有效 + if ((tmp.match(/[<>]/g) || []).length <= 0) return true; + // 有标签的需能被 jQuery 解析 + return $(tmp).length > 0; + } catch (ex) { + return false; + } + }, + + /** + * 处理配置项(合并默认值、格式化参数) + * 将用户配置与默认配置合并,并转换为内部可用的格式 + * @param {Object} opts 用户配置 + */ + _process_options: function(opts) { + this._o = $.extend({}, this._o, opts); // 存储原始配置(未处理的) + var o = this.o = $.extend({}, this._o); // 处理后的配置 + + // 处理语言(支持降级,如 'de-DE' → 'de' → 'en') + var lang = o.language; + if (!dates[lang]) { + lang = lang.split('-')[0]; // 取主语言(如 'de-DE' → 'de') + if (!dates[lang]) lang = defaults.language; // 最终降级到默认语言 + } + o.language = lang; + + // 处理视图模式(确保在 min/max 范围内) + o.startView = this._resolveViewName(o.startView); + o.minViewMode = this._resolveViewName(o.minViewMode); + o.maxViewMode = this._resolveViewName(o.maxViewMode); + // 限制 startView 在 [minViewMode, maxViewMode] 之间 + o.startView = Math.max(this.o.minViewMode, Math.min(this.o.maxViewMode, o.startView)); + + // 处理多日期选择(true=不限数量,数字=限制数量) + if (o.multidate !== true) { + o.multidate = Number(o.multidate) || false; + if (o.multidate !== false) o.multidate = Math.max(0, o.multidate); // 确保非负 + } + o.multidateSeparator = String(o.multidateSeparator); // 分隔符转为字符串 + + // 处理星期起始日(0-6,默认 0=周日) + o.weekStart %= 7; // 取模确保在 0-6 范围内 + o.weekEnd = (o.weekStart + 6) % 7; // 计算周末(起始日+6天) + + // 处理日期范围(startDate/endDate 转为 UTC 日期) + var format = DPGlobal.parseFormat(o.format); + // 处理开始日期 + if (o.startDate !== -Infinity) { + if (!!o.startDate) { + if (o.startDate instanceof Date) + o.startDate = this._local_to_utc(this._zero_time(o.startDate)); + else // 字符串转日期 + o.startDate = DPGlobal.parseDate(o.startDate, format, o.language, o.assumeNearbyYear); + } else { + o.startDate = -Infinity; // 无限制 + } + } + // 处理结束日期(逻辑同上) + if (o.endDate !== Infinity) { + if (!!o.endDate) { + if (o.endDate instanceof Date) + o.endDate = this._local_to_utc(this._zero_time(o.endDate)); + else + o.endDate = DPGlobal.parseDate(o.endDate, format, o.language, o.assumeNearbyYear); + } else { + o.endDate = Infinity; // 无限制 + } + } + + // 处理禁用/高亮星期、禁用日期 + o.daysOfWeekDisabled = this._resolveDaysOfWeek(o.daysOfWeekDisabled || []); + o.daysOfWeekHighlighted = this._resolveDaysOfWeek(o.daysOfWeekHighlighted || []); + o.datesDisabled = o.datesDisabled || []; + if (!$.isArray(o.datesDisabled)) o.datesDisabled = o.datesDisabled.split(','); // 字符串转数组 + // 转换为 UTC 日期 + o.datesDisabled = $.map(o.datesDisabled, function(d) { + return DPGlobal.parseDate(d, format, o.language, o.assumeNearbyYear); + }); + + // 处理定位(orientation:auto/left/right/top/bottom) + var plc = String(o.orientation).toLowerCase().split(/\s+/g); + plc = $.grep(plc, function(word) { // 过滤无效值 + return /^auto|left|right|top|bottom$/.test(word); + }); + o.orientation = { x: 'auto', y: 'auto' }; // 默认值 + if (plc.length === 1) { + // 单值处理(如 'top' → y轴top) + switch (plc[0]) { + case 'top': case 'bottom': o.orientation.y = plc[0]; break; + case 'left': case 'right': o.orientation.x = plc[0]; break; + } + } else { + // 多值处理(如 'left top' → x左y上) + o.orientation.x = $.grep(plc, function(w) { return /left|right/.test(w); })[0] || 'auto'; + o.orientation.y = $.grep(plc, function(w) { return /top|bottom/.test(w); })[0] || 'auto'; + } + + // 处理默认视图日期(支持 Date/字符串/对象) + if (o.defaultViewDate instanceof Date || typeof o.defaultViewDate === 'string') { + o.defaultViewDate = DPGlobal.parseDate(o.defaultViewDate, format, o.language, o.assumeNearbyYear); + } else if (o.defaultViewDate) { + // 对象格式(如 {year:2024, month:5, day:1}) + var year = o.defaultViewDate.year || new Date().getFullYear(); + var month = o.defaultViewDate.month || 0; + var day = o.defaultViewDate.day || 1; + o.defaultViewDate = UTCDate(year, month, day); + } else { + o.defaultViewDate = UTCToday(); // 默认今天 + } + }, + + // 事件管理(绑定/解绑) + _events: [], // 主事件(输入框、按钮等) + _secondaryEvents: [], // 次要事件(日历内部、窗口等) + + /** + * 应用事件绑定 + * @param {Array} evs 事件配置数组,格式:[元素, 选择器, 事件映射] 或 [元素, 事件映射] + */ + _applyEvents: function(evs) { + for (var i = 0, el, ch, ev; i < evs.length; i++) { + el = evs[i][0]; + if (evs[i].length === 2) { + ch = undefined; + ev = evs[i][1]; + } else if (evs[i].length === 3) { + ch = evs[i][1]; + ev = evs[i][2]; + } + el.on(ev, ch); // 绑定事件 + } + }, + + /** + * 解除事件绑定 + * @param {Array} evs 事件配置数组(同 _applyEvents) + */ + _unapplyEvents: function(evs) { + for (var i = 0, el, ev, ch; i < evs.length; i++) { + el = evs[i][0]; + if (evs[i].length === 2) { + ch = undefined; + ev = evs[i][1]; + } else if (evs[i].length === 3) { + ch = evs[i][1]; + ev = evs[i][2]; + } + el.off(ev, ch); // 解绑事件 + } + }, + + /** + * 构建事件逻辑(区分输入框类型绑定不同事件) + */ + _buildEvents: function() { + // 输入框通用事件 var events = { - keyup: $.proxy(function(e){ + keyup: $.proxy(function(e) { // 输入内容变化时更新 + // 忽略方向键、回车等导航键(避免频繁更新) if ($.inArray(e.keyCode, [27, 37, 39, 38, 40, 32, 13, 9]) === -1) this.update(); }, this), - keydown: $.proxy(this.keydown, this), - paste: $.proxy(this.paste, this) + keydown: $.proxy(this.keydown, this), // 键盘导航 + paste: $.proxy(this.paste, this) // 粘贴日期 }; + // 聚焦时显示日历(根据配置) if (this.o.showOnFocus === true) { events.focus = $.proxy(this.show, this); } - if (this.isInput) { // single input + // 根据元素类型绑定事件 + if (this.isInput) { // 单个输入框 + this._events = [[this.element, events]]; + } else if (this.component && this.inputField.length) { // 输入框+按钮组件 this._events = [ - [this.element, events] + [this.inputField, events], // 输入框事件 + [this.component, { click: $.proxy(this.show, this) }] // 按钮点击显示 ]; + } else { // 内联 div + this._events = [[this.element, { + click: $.proxy(this.show, this), + keydown: $.proxy(this.keydown, this) + }]]; } - // component: input + button - else if (this.component && this.inputField.length) { - this._events = [ - // For components that are not readonly, allow keyboard nav - [this.inputField, events], - [this.component, { - click: $.proxy(this.show, this) - }] - ]; + + // 公共事件:失焦处理(记录失焦源) + this._events.push( + [this.element, '*', { blur: $.proxy(function(e) { this._focused_from = e.target; }, this) }], + [this.element, { blur: $.proxy(function(e) { this._focused_from = e.target; }, this) }] + ); + + // 即时更新(年月变化时立即更新输入框) + if (this.o.immediateUpdates) { + this._events.push([this.element, { + 'changeYear changeMonth': $.proxy(function(e) { this.update(e.date); }, this) + }]); } - else { - this._events = [ - [this.element, { - click: $.proxy(this.show, this), - keydown: $.proxy(this.keydown, this) - }] - ]; - } - this._events.push( - // Component: listen for blur on element descendants - [this.element, '*', { - blur: $.proxy(function(e){ - this._focused_from = e.target; - }, this) - }], - // Input: listen for blur on element - [this.element, { - blur: $.proxy(function(e){ - this._focused_from = e.target; - }, this) - }] - ); - - if (this.o.immediateUpdates) { - // Trigger input updates immediately on changed year/month - this._events.push([this.element, { - 'changeYear changeMonth': $.proxy(function(e){ - this.update(e.date); - }, this) - }]); - } - - this._secondaryEvents = [ - [this.picker, { - click: $.proxy(this.click, this) - }], - [this.picker, '.prev, .next', { - click: $.proxy(this.navArrowsClick, this) - }], - [this.picker, '.day:not(.disabled)', { - click: $.proxy(this.dayCellClick, this) - }], - [$(window), { - resize: $.proxy(this.place, this) - }], - [$(document), { - 'mousedown touchstart': $.proxy(function(e){ - // Clicked outside the datepicker, hide it - if (!( - this.element.is(e.target) || - this.element.find(e.target).length || - this.picker.is(e.target) || - this.picker.find(e.target).length || - this.isInline - )){ - this.hide(); - } - }, this) - }] - ]; - }, - _attachEvents: function(){ - this._detachEvents(); - this._applyEvents(this._events); - }, - _detachEvents: function(){ - this._unapplyEvents(this._events); - }, - _attachSecondaryEvents: function(){ - this._detachSecondaryEvents(); - this._applyEvents(this._secondaryEvents); - }, - _detachSecondaryEvents: function(){ - this._unapplyEvents(this._secondaryEvents); - }, - _trigger: function(event, altdate){ - var date = altdate || this.dates.get(-1), - local_date = this._utc_to_local(date); - - this.element.trigger({ - type: event, - date: local_date, - viewMode: this.viewMode, - dates: $.map(this.dates, this._utc_to_local), - format: $.proxy(function(ix, format){ - if (arguments.length === 0){ - ix = this.dates.length - 1; - format = this.o.format; - } else if (typeof ix === 'string'){ - format = ix; - ix = this.dates.length - 1; - } - format = format || this.o.format; - var date = this.dates.get(ix); - return DPGlobal.formatDate(date, format, this.o.language); - }, this) - }); - }, - - show: function(){ - if (this.inputField.prop('disabled') || (this.inputField.prop('readonly') && this.o.enableOnReadonly === false)) - return; - if (!this.isInline) - this.picker.appendTo(this.o.container); - this.place(); - this.picker.show(); - this._attachSecondaryEvents(); - this._trigger('show'); - if ((window.navigator.msMaxTouchPoints || 'ontouchstart' in document) && this.o.disableTouchKeyboard) { - $(this.element).blur(); - } - return this; - }, - - hide: function(){ - if (this.isInline || !this.picker.is(':visible')) - return this; - this.focusDate = null; - this.picker.hide().detach(); - this._detachSecondaryEvents(); - this.setViewMode(this.o.startView); - - if (this.o.forceParse && this.inputField.val()) - this.setValue(); - this._trigger('hide'); - return this; - }, - - destroy: function(){ - this.hide(); - this._detachEvents(); - this._detachSecondaryEvents(); - this.picker.remove(); - delete this.element.data().datepicker; - if (!this.isInput){ - delete this.element.data().date; - } - return this; - }, - - paste: function(e){ - var dateString; - if (e.originalEvent.clipboardData && e.originalEvent.clipboardData.types - && $.inArray('text/plain', e.originalEvent.clipboardData.types) !== -1) { - dateString = e.originalEvent.clipboardData.getData('text/plain'); - } else if (window.clipboardData) { - dateString = window.clipboardData.getData('Text'); - } else { - return; - } - this.setDate(dateString); - this.update(); - e.preventDefault(); - }, - - _utc_to_local: function(utc){ - if (!utc) { - return utc; - } - - var local = new Date(utc.getTime() + (utc.getTimezoneOffset() * 60000)); - - if (local.getTimezoneOffset() !== utc.getTimezoneOffset()) { - local = new Date(utc.getTime() + (local.getTimezoneOffset() * 60000)); - } - - return local; - }, - _local_to_utc: function(local){ - return local && new Date(local.getTime() - (local.getTimezoneOffset()*60000)); - }, - _zero_time: function(local){ - return local && new Date(local.getFullYear(), local.getMonth(), local.getDate()); - }, - _zero_utc_time: function(utc){ - return utc && UTCDate(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate()); - }, - - getDates: function(){ - return $.map(this.dates, this._utc_to_local); - }, - - getUTCDates: function(){ - return $.map(this.dates, function(d){ - return new Date(d); - }); - }, - - getDate: function(){ - return this._utc_to_local(this.getUTCDate()); - }, - - getUTCDate: function(){ - var selected_date = this.dates.get(-1); - if (selected_date !== undefined) { - return new Date(selected_date); - } else { - return null; - } - }, - - clearDates: function(){ - this.inputField.val(''); - this.update(); - this._trigger('changeDate'); - - if (this.o.autoclose) { - this.hide(); - } - }, - - setDates: function(){ - var args = $.isArray(arguments[0]) ? arguments[0] : arguments; - this.update.apply(this, args); - this._trigger('changeDate'); - this.setValue(); - return this; - }, - - setUTCDates: function(){ - var args = $.isArray(arguments[0]) ? arguments[0] : arguments; - this.setDates.apply(this, $.map(args, this._utc_to_local)); - return this; - }, - - setDate: alias('setDates'), - setUTCDate: alias('setUTCDates'), - remove: alias('destroy', 'Method `remove` is deprecated and will be removed in version 2.0. Use `destroy` instead'), - - setValue: function(){ - var formatted = this.getFormattedDate(); - this.inputField.val(formatted); - return this; - }, - - getFormattedDate: function(format){ - if (format === undefined) - format = this.o.format; - - var lang = this.o.language; - return $.map(this.dates, function(d){ - return DPGlobal.formatDate(d, format, lang); - }).join(this.o.multidateSeparator); - }, - - getStartDate: function(){ - return this.o.startDate; - }, - - setStartDate: function(startDate){ - this._process_options({startDate: startDate}); - this.update(); - this.updateNavArrows(); - return this; - }, - - getEndDate: function(){ - return this.o.endDate; - }, - - setEndDate: function(endDate){ - this._process_options({endDate: endDate}); - this.update(); - this.updateNavArrows(); - return this; - }, - - setDaysOfWeekDisabled: function(daysOfWeekDisabled){ - this._process_options({daysOfWeekDisabled: daysOfWeekDisabled}); - this.update(); - return this; - }, - - setDaysOfWeekHighlighted: function(daysOfWeekHighlighted){ - this._process_options({daysOfWeekHighlighted: daysOfWeekHighlighted}); - this.update(); - return this; - }, - - setDatesDisabled: function(datesDisabled){ - this._process_options({datesDisabled: datesDisabled}); - this.update(); - return this; - }, - - place: function(){ - if (this.isInline) - return this; - var calendarWidth = this.picker.outerWidth(), - calendarHeight = this.picker.outerHeight(), - visualPadding = 10, - container = $(this.o.container), - windowWidth = container.width(), - scrollTop = this.o.container === 'body' ? $(document).scrollTop() : container.scrollTop(), - appendOffset = container.offset(); - - var parentsZindex = [0]; - this.element.parents().each(function(){ - var itemZIndex = $(this).css('z-index'); - if (itemZIndex !== 'auto' && Number(itemZIndex) !== 0) parentsZindex.push(Number(itemZIndex)); - }); - var zIndex = Math.max.apply(Math, parentsZindex) + this.o.zIndexOffset; - var offset = this.component ? this.component.parent().offset() : this.element.offset(); - var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(false); - var width = this.component ? this.component.outerWidth(true) : this.element.outerWidth(false); - var left = offset.left - appendOffset.left; - var top = offset.top - appendOffset.top; - - if (this.o.container !== 'body') { - top += scrollTop; - } - - this.picker.removeClass( - 'datepicker-orient-top datepicker-orient-bottom '+ - 'datepicker-orient-right datepicker-orient-left' - ); - - if (this.o.orientation.x !== 'auto'){ - this.picker.addClass('datepicker-orient-' + this.o.orientation.x); - if (this.o.orientation.x === 'right') - left -= calendarWidth - width; - } - // auto x orientation is best-placement: if it crosses a window - // edge, fudge it sideways - else { - if (offset.left < 0) { - // component is outside the window on the left side. Move it into visible range - this.picker.addClass('datepicker-orient-left'); - left -= offset.left - visualPadding; - } else if (left + calendarWidth > windowWidth) { - // the calendar passes the widow right edge. Align it to component right side - this.picker.addClass('datepicker-orient-right'); - left += width - calendarWidth; - } else { - if (this.o.rtl) { - // Default to right - this.picker.addClass('datepicker-orient-right'); - } else { - // Default to left - this.picker.addClass('datepicker-orient-left'); - } - } - } - - // auto y orientation is best-situation: top or bottom, no fudging, - // decision based on which shows more of the calendar - var yorient = this.o.orientation.y, - top_overflow; - if (yorient === 'auto'){ - top_overflow = -scrollTop + top - calendarHeight; - yorient = top_overflow < 0 ? 'bottom' : 'top'; - } - - this.picker.addClass('datepicker-orient-' + yorient); - if (yorient === 'top') - top -= calendarHeight + parseInt(this.picker.css('padding-top')); - else - top += height; - - if (this.o.rtl) { - var right = windowWidth - (left + width); - this.picker.css({ - top: top, - right: right, - zIndex: zIndex - }); - } else { - this.picker.css({ - top: top, - left: left, - zIndex: zIndex - }); - } - return this; - }, - - _allow_update: true, - update: function(){ - if (!this._allow_update) - return this; - - var oldDates = this.dates.copy(), - dates = [], - fromArgs = false; - if (arguments.length){ - $.each(arguments, $.proxy(function(i, date){ - if (date instanceof Date) - date = this._local_to_utc(date); - dates.push(date); - }, this)); - fromArgs = true; - } else { - dates = this.isInput - ? this.element.val() - : this.element.data('date') || this.inputField.val(); - if (dates && this.o.multidate) - dates = dates.split(this.o.multidateSeparator); - else - dates = [dates]; - delete this.element.data().date; - } - - dates = $.map(dates, $.proxy(function(date){ - return DPGlobal.parseDate(date, this.o.format, this.o.language, this.o.assumeNearbyYear); - }, this)); - dates = $.grep(dates, $.proxy(function(date){ - return ( - !this.dateWithinRange(date) || - !date - ); - }, this), true); - this.dates.replace(dates); - - if (this.o.updateViewDate) { - if (this.dates.length) - this.viewDate = new Date(this.dates.get(-1)); - else if (this.viewDate < this.o.startDate) - this.viewDate = new Date(this.o.startDate); - else if (this.viewDate > this.o.endDate) - this.viewDate = new Date(this.o.endDate); - else - this.viewDate = this.o.defaultViewDate; - } - - if (fromArgs){ - // setting date by clicking - this.setValue(); - this.element.change(); - } - else if (this.dates.length){ - // setting date by typing - if (String(oldDates) !== String(this.dates) && fromArgs) { - this._trigger('changeDate'); - this.element.change(); - } - } - if (!this.dates.length && oldDates.length) { - this._trigger('clearDate'); - this.element.change(); - } - - this.fill(); - return this; - }, - - fillDow: function(){ - if (this.o.showWeekDays) { - var dowCnt = this.o.weekStart, - html = ''; - if (this.o.calendarWeeks){ - html += ' '; - } - while (dowCnt < this.o.weekStart + 7){ - html += ''+dates[this.o.language].daysMin[(dowCnt++)%7]+''; - } - html += ''; - this.picker.find('.datepicker-days thead').append(html); - } - }, - - fillMonths: function(){ - var localDate = this._utc_to_local(this.viewDate); - var html = ''; - var focused; - for (var i = 0; i < 12; i++){ - focused = localDate && localDate.getMonth() === i ? ' focused' : ''; - html += '' + dates[this.o.language].monthsShort[i] + ''; - } - this.picker.find('.datepicker-months td').html(html); - }, - - setRange: function(range){ - if (!range || !range.length) - delete this.range; - else - this.range = $.map(range, function(d){ - return d.valueOf(); - }); - this.fill(); - }, - - getClassNames: function(date){ - var cls = [], - year = this.viewDate.getUTCFullYear(), - month = this.viewDate.getUTCMonth(), - today = UTCToday(); - if (date.getUTCFullYear() < year || (date.getUTCFullYear() === year && date.getUTCMonth() < month)){ - cls.push('old'); - } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() === year && date.getUTCMonth() > month)){ - cls.push('new'); - } - if (this.focusDate && date.valueOf() === this.focusDate.valueOf()) - cls.push('focused'); - // Compare internal UTC date with UTC today, not local today - if (this.o.todayHighlight && isUTCEquals(date, today)) { - cls.push('today'); - } - if (this.dates.contains(date) !== -1) - cls.push('active'); - if (!this.dateWithinRange(date)){ - cls.push('disabled'); - } - if (this.dateIsDisabled(date)){ - cls.push('disabled', 'disabled-date'); - } - if ($.inArray(date.getUTCDay(), this.o.daysOfWeekHighlighted) !== -1){ - cls.push('highlighted'); - } - - if (this.range){ - if (date > this.range[0] && date < this.range[this.range.length-1]){ - cls.push('range'); - } - if ($.inArray(date.valueOf(), this.range) !== -1){ - cls.push('selected'); - } - if (date.valueOf() === this.range[0]){ - cls.push('range-start'); - } - if (date.valueOf() === this.range[this.range.length-1]){ - cls.push('range-end'); + + // 日历内部事件 + this._secondaryEvents = [ + [this.picker, { click: $.proxy(this.click, this) }], // 日历点击 + [this.picker, '.prev, .next', { click: $.proxy(this.navArrowsClick, this) }], // 箭头导航 + [this.picker, '.day:not(.disabled)', { click: $.proxy(this.dayCellClick, this) }], // 日期点击 + [$(window), { resize: $.proxy(this.place, this) }], // 窗口 resize 重定位 + [$(document), { // 点击外部隐藏 + 'mousedown touchstart': $.proxy(function(e) { + // 判断点击是否在元素、输入框、日历内部 + if (!( + this.element.is(e.target) || + this.element.find(e.target).length || + this.picker.is(e.target) || + this.picker.find(e.target).length || + this.isInline // 内联日历不隐藏 + )) { + this.hide(); + } + }, this) + }] + ]; + }, + + /** + * 绑定主事件 + */ + _attachEvents: function() { + this._detachEvents(); // 先解绑避免重复绑定 + this._applyEvents(this._events); + }, + + /** + * 解绑主事件 + */ + _detachEvents: function() { + this._unapplyEvents(this._events); + }, + + /** + * 绑定日历内部事件 + */ + _attachSecondaryEvents: function() { + this._detachSecondaryEvents(); + this._applyEvents(this._secondaryEvents); + }, + + /** + * 解绑日历内部事件 + */ + _detachSecondaryEvents: function() { + this._unapplyEvents(this._secondaryEvents); + }, + + /** + * 触发事件(封装日期格式转换) + * 统一事件触发逻辑,确保传递正确的日期格式 + * @param {string} event 事件名(如 'changeDate') + * @param {Date} altdate 事件关联的日期(默认使用最后选中的日期) + */ + _trigger: function(event, altdate) { + var date = altdate || this.dates.get(-1), + local_date = this._utc_to_local(date); // 转为本地日期 + + this.element.trigger({ + type: event, + date: local_date, // 当前日期 + viewMode: this.viewMode, // 当前视图模式 + dates: $.map(this.dates, this._utc_to_local), // 所有选中日期 + format: $.proxy(function(ix, format) { // 格式化方法 + if (arguments.length === 0) { + ix = this.dates.length - 1; // 默认最后一个 + format = this.o.format; + } else if (typeof ix === 'string') { + format = ix; // 支持 format(dateFormat) 调用 + ix = this.dates.length - 1; + } + format = format || this.o.format; + var date = this.dates.get(ix); + return DPGlobal.formatDate(date, format, this.o.language); + }, this) + }); + }, + + /** + * 显示日历 + * @returns {Datepicker} 实例(支持链式调用) + */ + show: function() { + // 禁用或只读(且不允许启用)时不显示 + if (this.inputField.prop('disabled') || (this.inputField.prop('readonly') && !this.o.enableOnReadonly)) + return; + // 非内联日历添加到容器 + if (!this.isInline) this.picker.appendTo(this.o.container); + this.place(); // 定位日历 + this.picker.show(); // 显示 + this._attachSecondaryEvents(); // 绑定内部事件 + this._trigger('show'); // 触发显示事件 + + // 触摸设备禁用虚拟键盘(避免遮挡) + if ((window.navigator.msMaxTouchPoints || 'ontouchstart' in document) && this.o.disableTouchKeyboard) { + $(this.element).blur(); + } + return this; + }, + + /** + * 隐藏日历 + * @returns {Datepicker} 实例(支持链式调用) + */ + hide: function() { + if (this.isInline || !this.picker.is(':visible')) return this; + this.focusDate = null; // 清除聚焦 + this.picker.hide().detach(); // 隐藏并移除 + this._detachSecondaryEvents(); // 解绑内部事件 + this.setViewMode(this.o.startView); // 重置视图模式 + + // 强制解析输入框内容(如果配置了) + if (this.o.forceParse && this.inputField.val()) + this.setValue(); + this._trigger('hide'); // 触发隐藏事件 + return this; + }, + + /** + * 销毁插件(移除DOM和事件) + * @returns {Datepicker} 实例 + */ + destroy: function() { + this.hide(); // 先隐藏 + this._detachEvents(); // 解绑主事件 + this._detachSecondaryEvents(); // 解绑内部事件 + this.picker.remove(); // 移除日历DOM + delete this.element.data().datepicker; // 清除数据 + if (!this.isInput) delete this.element.data().date; + return this; + }, + + /** + * 处理粘贴事件(解析粘贴的日期字符串) + * @param {Event} e 粘贴事件 + */ + paste: function(e) { + var dateString; + // 现代浏览器获取粘贴内容 + if (e.originalEvent.clipboardData && e.originalEvent.clipboardData.types && + $.inArray('text/plain', e.originalEvent.clipboardData.types) !== -1) { + dateString = e.originalEvent.clipboardData.getData('text/plain'); + } else if (window.clipboardData) { // IE 兼容 + dateString = window.clipboardData.getData('Text'); + } else { + return; + } + this.setDate(dateString); // 设置日期 + this.update(); // 更新日历 + e.preventDefault(); // 阻止默认粘贴(避免重复输入) + }, + + /** + * UTC 日期转本地日期 + * 解决时区差异:UTC 日期 + 时区偏移 = 本地日期 + * @param {Date} utc UTC 日期 + * @returns {Date} 本地日期 + */ + _utc_to_local: function(utc) { + if (!utc) return utc; + // UTC 时间戳 + 时区偏移(分钟转毫秒) + var local = new Date(utc.getTime() + (utc.getTimezoneOffset() * 60000)); + // 二次校验(处理跨时区日期变更) + if (local.getTimezoneOffset() !== utc.getTimezoneOffset()) { + local = new Date(utc.getTime() + (local.getTimezoneOffset() * 60000)); + } + return local; + }, + + /** + * 本地日期转 UTC 日期 + * 本地日期 - 时区偏移 = UTC 日期 + * @param {Date} local 本地日期 + * @returns {Date} UTC 日期 + */ + _local_to_utc: function(local) { + return local && new Date(local.getTime() - (local.getTimezoneOffset() * 60000)); + }, + + /** + * 本地日期时间归零(仅保留年月日) + * @param {Date} local 本地日期 + * @returns {Date} 时间归零的日期(如 2024-06-01 00:00:00) + */ + _zero_time: function(local) { + return local && new Date(local.getFullYear(), local.getMonth(), local.getDate()); + }, + + /** + * UTC 日期时间归零(仅保留年月日) + * @param {Date} utc UTC 日期 + * @returns {Date} 时间归零的 UTC 日期 + */ + _zero_utc_time: function(utc) { + return utc && UTCDate(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate()); + }, + + /** + * 获取选中的本地日期数组 + * @returns {Date[]} 本地日期数组 + */ + getDates: function() { + return $.map(this.dates, this._utc_to_local); + }, + + /** + * 获取选中的 UTC 日期数组 + * @returns {Date[]} UTC 日期数组 + */ + getUTCDates: function() { + return $.map(this.dates, function(d) { return new Date(d); }); // 深拷贝 + }, + + /** + * 获取选中的本地日期(最后一个) + * @returns {Date|null} 本地日期 + */ + getDate: function() { + return this._utc_to_local(this.getUTCDate()); + }, + + /** + * 获取选中的 UTC 日期(最后一个) + * @returns {Date|null} UTC 日期 + */ + getUTCDate: function() { + var selected_date = this.dates.get(-1); + return selected_date ? new Date(selected_date) : null; // 深拷贝 + }, + + /** + * 清空选中的日期 + */ + clearDates: function() { + this.inputField.val(''); // 清空输入框 + this.update(); // 更新日历 + this._trigger('changeDate'); // 触发变更事件 + if (this.o.autoclose) this.hide(); // 自动关闭 + }, + + /** + * 设置选中的日期(支持多个) + * @returns {Datepicker} 实例 + */ + setDates: function() { + var args = $.isArray(arguments[0]) ? arguments[0] : arguments; // 支持数组或多个参数 + this.update.apply(this, args); // 更新日历 + this._trigger('changeDate'); // 触发事件 + this.setValue(); // 更新输入框 + return this; + }, + + /** + * 设置选中的 UTC 日期(支持多个) + * @returns {Datepicker} 实例 + */ + setUTCDates: function() { + var args = $.isArray(arguments[0]) ? arguments[0] : arguments; + // 转为本地日期后调用 setDates + this.setDates.apply(this, $.map(args, this._utc_to_local)); + return this; + }, + + // 别名方法(兼容旧版本) + setDate: alias('setDates'), // setDate 指向 setDates + remove: alias('destroy', 'Method `remove` is deprecated, use `destroy` instead'), // 弃用提示 + + /** + * 更新输入框显示(格式化选中的日期) + * @returns {Datepicker} 实例 + */ + setValue: function() { + var formatted = this.getFormattedDate(); // 获取格式化字符串 + this.inputField.val(formatted); // 设置输入框值 + return this; + }, + + /** + * 获取格式化的日期字符串 + * @param {string} format 格式(默认使用配置的 format) + * @returns {string} 格式化后的字符串 + */ + getFormattedDate: function(format) { + if (format === undefined) format = this.o.format; + var lang = this.o.language; + // 多日期用分隔符拼接 + return $.map(this.dates, function(d) { + return DPGlobal.formatDate(d, format, lang); + }).join(this.o.multidateSeparator); + }, + + /** + * 获取开始日期限制 + * @returns {Date|number} 开始日期(-Infinity 表示无限制) + */ + getStartDate: function() { + return this.o.startDate; + }, + + /** + * 设置开始日期限制 + * @param {Date|string} startDate 开始日期 + * @returns {Datepicker} 实例 + */ + setStartDate: function(startDate) { + this._process_options({ startDate: startDate }); // 更新配置 + this.update(); // 刷新日历 + this.updateNavArrows(); // 更新导航箭头状态 + return this; + }, + + /** + * 获取结束日期限制 + * @returns {Date|number} 结束日期(Infinity 表示无限制) + */ + getEndDate: function() { + return this.o.endDate; + }, + + /** + * 设置结束日期限制 + * @param {Date|string} endDate 结束日期 + * @returns {Datepicker} 实例 + */ + setEndDate: function(endDate) { + this._process_options({ endDate: endDate }); + this.update(); + this.updateNavArrows(); + return this; + }, + + /** + * 设置禁用的星期 + * @param {string|number[]} daysOfWeekDisabled 禁用的星期(如 "0,6") + * @returns {Datepicker} 实例 + */ + setDaysOfWeekDisabled: function(daysOfWeekDisabled) { + this._process_options({ daysOfWeekDisabled: daysOfWeekDisabled }); + this.update(); + return this; + }, + + /** + * 设置高亮的星期 + * @param {string|number[]} daysOfWeekHighlighted 高亮的星期 + * @returns {Datepicker} 实例 + */ + setDaysOfWeekHighlighted: function(daysOfWeekHighlighted) { + this._process_options({ daysOfWeekHighlighted: daysOfWeekHighlighted }); + this.update(); + return this; + }, + + /** + * 设置禁用的具体日期 + * @param {string|Date[]} datesDisabled 禁用的日期 + * @returns {Datepicker} 实例 + */ + setDatesDisabled: function(datesDisabled) { + this._process_options({ datesDisabled: datesDisabled }); + this.update(); + return this; + }, + + /** + * 定位日历(根据配置和元素位置) + * 确保日历显示在合理位置,不超出视口 + * @returns {Datepicker} 实例 + */ + place: function() { + if (this.isInline) return this; // 内联无需定位 + + var calendarWidth = this.picker.outerWidth(), // 日历宽度 + calendarHeight = this.picker.outerHeight(), // 日历高度 + visualPadding = 10, // 视觉边距 + container = $(this.o.container), // 容器 + windowWidth = container.width(), // 容器宽度 + scrollTop = this.o.container === 'body' ? $(document).scrollTop() : container.scrollTop(), // 滚动位置 + appendOffset = container.offset(); // 容器偏移 + + // 计算 z-index(确保在父元素上方) + var parentsZindex = [0]; + this.element.parents().each(function() { + var z = $(this).css('z-index'); + if (z !== 'auto' && Number(z) !== 0) parentsZindex.push(Number(z)); + }); + var zIndex = Math.max.apply(Math, parentsZindex) + this.o.zIndexOffset; // 最大父级 z-index + 偏移 + + // 计算定位坐标 + var offset = this.component ? this.component.parent().offset() : this.element.offset(); + var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(false); + var width = this.component ? this.component.outerWidth(true) : this.element.outerWidth(false); + var left = offset.left - appendOffset.left; // 相对容器左偏移 + var top = offset.top - appendOffset.top; // 相对容器上偏移 + if (this.o.container !== 'body') top += scrollTop; // 容器内滚动修正 + + // 处理水平定位(left/right/auto) + this.picker.removeClass('datepicker-orient-top datepicker-orient-bottom datepicker-orient-right datepicker-orient-left'); + if (this.o.orientation.x !== 'auto') { + this.picker.addClass('datepicker-orient-' + this.o.orientation.x); + if (this.o.orientation.x === 'right') left -= calendarWidth - width; // 右对齐 + } else { + // 自动判断:左边界溢出则右对齐,否则左对齐 + if (offset.left < 0) { + this.picker.addClass('datepicker-orient-left'); + left -= offset.left - visualPadding; + } else if (left + calendarWidth > windowWidth) { + this.picker.addClass('datepicker-orient-right'); + left += width - calendarWidth; + } else { + this.picker.addClass(this.o.rtl ? 'datepicker-orient-right' : 'datepicker-orient-left'); + } + } + + // 处理垂直定位(top/bottom/auto) + var yorient = this.o.orientation.y; + if (yorient === 'auto') { + // 自动判断:上边界溢出则下对齐,否则上对齐 + var top_overflow = -scrollTop + top - calendarHeight; + yorient = top_overflow < 0 ? 'bottom' : 'top'; + } + this.picker.addClass('datepicker-orient-' + yorient); + if (yorient === 'top') top -= calendarHeight + parseInt(this.picker.css('padding-top')); // 上对齐 + else top += height; // 下对齐 + + // 应用定位样式(RTL 布局特殊处理) + if (this.o.rtl) { + this.picker.css({ top: top, right: windowWidth - (left + width), zIndex: zIndex }); + } else { + this.picker.css({ top: top, left: left, zIndex: zIndex }); + } + return this; + }, + + _allow_update: true, // 控制是否允许更新(初始化时临时禁用) + + /** + * 更新日历状态(解析输入框内容或外部设置的日期) + * @returns {Datepicker} 实例 + */ + update: function() { + if (!this._allow_update) return this; + + var oldDates = this.dates.copy(), // 保存旧日期(用于判断是否变更) + dates = [], + fromArgs = false; // 是否从参数更新 + + // 处理参数(直接传入日期) + if (arguments.length) { + $.each(arguments, $.proxy(function(i, date) { + if (date instanceof Date) date = this._local_to_utc(date); // 转为 UTC + dates.push(date); + }, this)); + fromArgs = true; + } else { + // 从输入框或数据属性读取日期 + dates = this.isInput ? this.element.val() : this.element.data('date') || this.inputField.val(); + if (dates && this.o.multidate) dates = dates.split(this.o.multidateSeparator); // 多日期分割 + else dates = [dates]; + delete this.element.data().date; // 清除临时数据 + } + + // 解析日期字符串为 Date 对象 + dates = $.map(dates, $.proxy(function(date) { + return DPGlobal.parseDate(date, this.o.format, this.o.language, this.o.assumeNearbyYear); + }, this)); + // 过滤无效或超出范围的日期 + dates = $.grep(dates, $.proxy(function(date) { + return this.dateWithinRange(date) && date; + }, this)); + this.dates.replace(dates); // 更新选中日期 + + // 更新视图日期(viewDate) + if (this.o.updateViewDate) { + if (this.dates.length) this.viewDate = new Date(this.dates.get(-1)); // 有选中日期则用最后一个 + else if (this.viewDate < this.o.startDate) this.viewDate = new Date(this.o.startDate); // 不小于开始日期 + else if (this.viewDate > this.o.endDate) this.viewDate = new Date(this.o.endDate); // 不大于结束日期 + else this.viewDate = this.o.defaultViewDate; // 默认日期 + } + + // 触发事件和更新输入框 + if (fromArgs) { + this.setValue(); // 从参数更新时直接设置输入框 + this.element.change(); // 触发 change 事件 + } else if (this.dates.length) { + // 日期变更时触发事件 + if (String(oldDates) !== String(this.dates) && fromArgs) { + this._trigger('changeDate'); + this.element.change(); + } + } + // 清空日期时触发事件 + if (!this.dates.length && oldDates.length) { + this._trigger('clearDate'); + this.element.change(); + } + + this.fill(); // 重新渲染日历 + return this; + }, + + /** + * 渲染星期标题(如 Su、Mo、Tu) + */ + fillDow: function() { + if (!this.o.showWeekDays) return; // 不显示则退出 + var dowCnt = this.o.weekStart, // 起始星期(如 1=周一) + html = ''; + if (this.o.calendarWeeks) html += ' '; // 周数列(占位符) + // 循环生成 7 个星期标题 + while (dowCnt < this.o.weekStart + 7) { + html += '' + dates[this.o.language].daysMin[(dowCnt++) % 7] + ''; + } + html += ''; + this.picker.find('.datepicker-days thead').append(html); // 添加到表头 + }, + + /** + * 渲染月视图的月份列表(如 Jan、Feb) + */ + fillMonths: function() { + var localDate = this._utc_to_local(this.viewDate); // 转为本地日期 + var html = '', focused; + // 循环生成 12 个月份 + for (var i = 0; i < 12; i++) { + // 当前月份添加聚焦样式 + focused = localDate && localDate.getMonth() === i ? ' focused' : ''; + html += '' + dates[this.o.language].monthsShort[i] + ''; + } + this.picker.find('.datepicker-months td').html(html); // 添加到月视图 + }, + + /** + * 设置日期范围(用于高亮开始-结束之间的日期) + * 主要用于日期范围选择器(DateRangePicker) + * @param {Date[]} range 日期范围数组 + */ + setRange: function(range) { + if (!range || !range.length) delete this.range; // 清除范围 + else this.range = $.map(range, function(d) { return d.valueOf(); }); // 转为时间戳 + this.fill(); // 重新渲染 + }, + + /** + * 获取日期单元格的样式类 + * 根据日期状态(选中、今天、禁用等)返回对应的 CSS 类 + * @param {Date} date 日期 + * @returns {string[]} 样式类数组 + */ + getClassNames: function(date) { + var cls = [], + year = this.viewDate.getUTCFullYear(), + month = this.viewDate.getUTCMonth(), + today = UTCToday(); // 今天的 UTC 日期 + + // 区分上月/本月/下月日期 + if (date.getUTCFullYear() < year || (date.getUTCFullYear() === year && date.getUTCMonth() < month)) { + cls.push('old'); // 上月 + } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() === year && date.getUTCMonth() > month)) { + cls.push('new'); // 下月 + } + + // 聚焦状态(键盘导航) + if (this.focusDate && date.valueOf() === this.focusDate.valueOf()) cls.push('focused'); + // 今天高亮 + if (this.o.todayHighlight && isUTCEquals(date, today)) cls.push('today'); + // 选中状态 + if (this.dates.contains(date) !== -1) cls.push('active'); + // 超出范围(小于 startDate 或大于 endDate) + if (!this.dateWithinRange(date)) cls.push('disabled'); + // 禁用日期(禁用星期或在禁用列表中) + if (this.dateIsDisabled(date)) cls.push('disabled', 'disabled-date'); + // 高亮星期 + if ($.inArray(date.getUTCDay(), this.o.daysOfWeekHighlighted) !== -1) cls.push('highlighted'); + + // 范围样式(用于日期范围选择) + if (this.range) { + if (date > this.range[0] && date < this.range[this.range.length - 1]) cls.push('range'); // 中间日期 + if ($.inArray(date.valueOf(), this.range) !== -1) cls.push('selected'); // 选中的端点 + if (date.valueOf() === this.range[0]) cls.push('range-start'); // 开始 + if (date.valueOf() === this.range[this.range.length - 1]) cls.push('range-end'); // 结束 + } + + return cls; + }, + + /** + * 渲染年/十年/世纪视图 + * 通用方法,通过参数控制渲染层级 + * @param {string} selector 容器选择器 + * @param {string} cssClass 样式类(如 'year') + * @param {number} factor 范围因子(10=十年,100=世纪) + * @param {number} year 当前年份 + * @param {number} startYear 开始年份限制 + * @param {number} endYear 结束年份限制 + * @param {Function} beforeFn 自定义处理函数(如 beforeShowYear) + */ + _fill_yearsView: function(selector, cssClass, factor, year, startYear, endYear, beforeFn) { + var html = '', step = factor / 10, // 步长(如 factor=100 → step=10) + view = this.picker.find(selector), + startVal = Math.floor(year / factor) * factor, // 起始值(如 2024/100 → 2000) + endVal = startVal + step * 9, // 结束值(如 2000 + 10*9 = 2090) + focusedVal = Math.floor(this.viewDate.getFullYear() / step) * step, // 聚焦值 + selected = $.map(this.dates, function(d) { // 选中的年份 + return Math.floor(d.getUTCFullYear() / step) * step; + }); + + var classes, tooltip, before; + // 从 startVal - step 到 endVal + step(前后各多显示一个) + for (var currVal = startVal - step; currVal <= endVal + step; currVal += step) { + classes = [cssClass]; + tooltip = null; + + // 区分前后范围(非当前页的年份) + if (currVal === startVal - step) classes.push('old'); + else if (currVal === endVal + step) classes.push('new'); + // 选中状态 + if ($.inArray(currVal, selected) !== -1) classes.push('active'); + // 超出范围(小于 startYear 或大于 endYear) + if (currVal < startYear || currVal > endYear) classes.push('disabled'); + // 聚焦状态 + if (currVal === focusedVal) classes.push('focused'); + + // 自定义处理(beforeShowYear 等回调) + if (beforeFn !== $.noop) { + before = beforeFn(new Date(currVal, 0, 1)); // 传入当年第一天 + if (before === undefined) before = {}; + else if (typeof before === 'boolean') before = { enabled: before }; // 仅返回是否启用 + else if (typeof before === 'string') before = { classes: before }; // 仅返回样式 + if (before.enabled === false) classes.push('disabled'); // 禁用 + if (before.classes) classes = classes.concat(before.classes.split(/\s+/)); // 添加样式 + if (before.tooltip) tooltip = before.tooltip; // 提示信息 + } + + html += '' + currVal + ''; + } + + // 更新标题和内容 + view.find('.datepicker-switch').text(startVal + '-' + endVal); + view.find('td').html(html); + }, + + /** + * 渲染日历内容(根据当前视图模式) + * 核心渲染方法,根据视图模式(日/月/年等)生成对应的 HTML + */ + fill: function() { + var d = new Date(this.viewDate), + year = d.getUTCFullYear(), + month = d.getUTCMonth(), + // 解析开始/结束日期限制的年月 + startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, + startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, + endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, + endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, + // 本地化按钮文本 + todaytxt = dates[this.o.language].today || dates['en'].today || '', + cleartxt = dates[this.o.language].clear || dates['en'].clear || '', + titleFormat = dates[this.o.language].titleFormat || dates['en'].titleFormat; + + if (isNaN(year) || isNaN(month)) return; // 无效日期退出 + + // 更新标题和按钮 + this.picker.find('.datepicker-days .datepicker-switch').text(DPGlobal.formatDate(d, titleFormat, this.o.language)); + this.picker.find('tfoot .today').text(todaytxt).css('display', this.o.todayBtn ? 'table-cell' : 'none'); + this.picker.find('tfoot .clear').text(cleartxt).css('display', this.o.clearBtn ? 'table-cell' : 'none'); + this.picker.find('thead .datepicker-title').text(this.o.title).css('display', this.o.title ? 'table-cell' : 'none'); + + // 更新导航箭头状态(禁用/启用) + this.updateNavArrows(); + this.fillMonths(); // 刷新月视图 + + // 渲染日视图(核心逻辑) + var prevMonth = UTCDate(year, month, 0), // 上月最后一天(如 6月 → 5月31日) + day = prevMonth.getUTCDate(); + // 计算日历起始日期(当前月第一周的第一天,考虑星期起始日) + prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7) % 7); + // 计算结束日期(6周后,确保覆盖所有可能的显示日期) + var nextMonth = new Date(prevMonth); + if (prevMonth.getUTCFullYear() < 100) nextMonth.setUTCFullYear(prevMonth.getUTCFullYear()); // 年份修正(处理0-99年) + nextMonth.setUTCDate(nextMonth.getUTCDate() + 42); // 6周 = 42天 + nextMonth = nextMonth.valueOf(); // 转为时间戳 + + var html = [], weekDay, clsName; + // 循环生成42天的日期单元格 + while (prevMonth.valueOf() < nextMonth) { + weekDay = prevMonth.getUTCDay(); + // 新的一周开始(如星期起始日为周一,weekDay=1时开始新行) + if (weekDay === this.o.weekStart) { + html.push(''); + if (this.o.calendarWeeks) { // 渲染周数(ISO 8601 标准) + // 计算当前周的周一(用于周数计算) + var ws = new Date(+prevMonth + (this.o.weekStart - weekDay - 7) % 7 * 864e5); + // 计算当前周的周四(ISO周数以周四为参考) + var th = new Date(Number(ws) + (7 + 4 - ws.getUTCDay()) % 7 * 864e5); + // 计算当年第一周的周四 + var yth = new Date(Number(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay()) % 7 * 864e5); + // 计算周数 + var calWeek = (th - yth) / 864e5 / 7 + 1; + html.push('' + calWeek + ''); + } + } + + // 计算日期单元格样式 + clsName = this.getClassNames(prevMonth); + clsName.push('day'); // 基础样式 + var content = prevMonth.getUTCDate(); // 显示的日期数字 + var tooltip = null, before; + + // 应用 beforeShowDay 回调(自定义日期样式) + if (this.o.beforeShowDay !== $.noop) { + before = this.o.beforeShowDay(this._utc_to_local(prevMonth)); // 传入本地日期 + if (before === undefined) before = {}; + else if (typeof before === 'boolean') before = { enabled: before }; + else if (typeof before === 'string') before = { classes: before }; + if (before.enabled === false) clsName.push('disabled'); // 禁用 + if (before.classes) clsName = clsName.concat(before.classes.split(/\s+/)); // 添加样式 + if (before.tooltip) tooltip = before.tooltip; // 提示信息 + if (before.content) content = before.content; // 自定义内容(如图标) + } + + // 去重样式类 + clsName = $.isFunction($.uniqueSort) ? $.uniqueSort(clsName) : $.unique(clsName); + + // 添加日期单元格(存储时间戳用于后续解析) + html.push('' + content + ''); + + // 一周结束(如星期起始日为周一,weekDay=0时结束行) + if (weekDay === this.o.weekEnd) html.push(''); + + prevMonth.setUTCDate(prevMonth.getUTCDate() + 1); // 下一天 + } + this.picker.find('.datepicker-days tbody').html(html.join('')); // 更新日视图内容 + + // 渲染月视图(处理禁用和选中状态) + var monthsTitle = dates[this.o.language].monthsTitle || 'Months'; + var months = this.picker.find('.datepicker-months') + .find('.datepicker-switch').text(this.o.maxViewMode < 2 ? monthsTitle : year).end() // 更新标题 + .find('tbody span').removeClass('active'); // 清除选中状态 + + // 标记选中的月份 + $.each(this.dates, function(i, d) { + if (d.getUTCFullYear() === year) months.eq(d.getUTCMonth()).addClass('active'); + }); + // 处理禁用月份 + if (year < startYear || year > endYear) months.addClass('disabled'); + if (year === startYear) months.slice(0, startMonth).addClass('disabled'); // 开始年份前几个月 + if (year === endYear) months.slice(endMonth + 1).addClass('disabled'); // 结束年份后几个月 + + // 应用 beforeShowMonth 回调 + if (this.o.beforeShowMonth !== $.noop) { + var that = this; + $.each(months, function(i, month) { + var before = that.o.beforeShowMonth(new Date(year, i, 1)); // 传入当月第一天 + if (before === undefined) before = {}; + else if (typeof before === 'boolean') before = { enabled: before }; + else if (typeof before === 'string') before = { classes: before }; + if (before.enabled === false && !$(month).hasClass('disabled')) $(month).addClass('disabled'); + if (before.classes) $(month).addClass(before.classes); + if (before.tooltip) $(month).prop('title', before.tooltip); + }); + } + + // 渲染年/十年/世纪视图 + this._fill_yearsView('.datepicker-years', 'year', 10, year, startYear, endYear, this.o.beforeShowYear); + this._fill_yearsView('.datepicker-decades', 'decade', 100, year, startYear, endYear, this.o.beforeShowDecade); + this._fill_yearsView('.datepicker-centuries', 'century', 1000, year, startYear, endYear, this.o.beforeShowCentury); + }, + + /** + * 更新导航箭头状态(禁用/启用) + * 根据当前视图和日期范围限制,判断前后箭头是否可点击 + */ + updateNavArrows: function() { + if (!this._allow_update) return; + + var d = new Date(this.viewDate), + year = d.getUTCFullYear(), + month = d.getUTCMonth(), + // 解析开始/结束日期限制 + startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, + startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, + endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, + endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, + prevIsDisabled, nextIsDisabled, factor = 1; // factor 用于计算跨度 + + // 根据视图模式判断箭头是否禁用 + switch (this.viewMode) { + case 0: // 日视图(按月导航) + prevIsDisabled = year <= startYear && month <= startMonth; // 不能再往前 + nextIsDisabled = year >= endYear && month >= endMonth; // 不能再往后 + break; + case 4: factor *= 10; // 世纪视图(1000年跨度) + case 3: factor *= 10; // 十年视图(100年跨度) + case 2: factor *= 10; // 年视图(10年跨度) + case 1: // 月视图(1年跨度) + prevIsDisabled = Math.floor(year / factor) * factor <= startYear; + nextIsDisabled = Math.floor(year / factor) * factor + factor >= endYear; + break; + } + + // 更新箭头样式 + this.picker.find('.prev').toggleClass('disabled', prevIsDisabled); + this.picker.find('.next').toggleClass('disabled', nextIsDisabled); + }, + + /** + * 处理日历内部点击事件(切换视图、选择年月等) + * @param {Event} e 点击事件 + */ + click: function(e) { + e.preventDefault(); // 阻止默认行为 + e.stopPropagation(); // 阻止冒泡 + + var target = $(e.target); + + // 点击切换视图(如日视图 → 月视图) + if (target.hasClass('datepicker-switch') && this.viewMode !== this.o.maxViewMode) { + this.setViewMode(this.viewMode + 1); // 视图层级+1 + } + + // 点击“今天”按钮 + if (target.hasClass('today') && !target.hasClass('day')) { + this.setViewMode(0); // 切换到日视图 + // 'linked' 模式直接选中今天,否则仅定位到今天 + this._setDate(UTCToday(), this.o.todayBtn === 'linked' ? null : 'view'); + } + + // 点击“清空”按钮 + if (target.hasClass('clear')) { + this.clearDates(); // 清空日期 + } + + // 点击月份/年份/十年/世纪(未禁用状态) + if (!target.hasClass('disabled')) { + if (target.hasClass('month') || target.hasClass('year') || target.hasClass('decade') || target.hasClass('century')) { + this.viewDate.setUTCDate(1); // 重置日期为1号 + var day = 1, month, year; + + if (this.viewMode === 1) { // 月视图 → 日视图 + month = target.parent().find('span').index(target); // 获取月份索引 + year = this.viewDate.getUTCFullYear(); + this.viewDate.setUTCMonth(month); // 更新月份 + } else { // 年/十年/世纪视图 → 下一级视图 + month = 0; // 默认为1月 + year = Number(target.text()); // 获取年份 + this.viewDate.setUTCFullYear(year); // 更新年份 + } + + // 触发视图变更事件 + this._trigger(DPGlobal.viewModes[this.viewMode - 1].e, this.viewDate); + + if (this.viewMode === this.o.minViewMode) { // 最小视图直接选中 + this._setDate(UTCDate(year, month, day)); + } else { // 切换到下一级视图 + this.setViewMode(this.viewMode - 1); // 视图层级-1 + this.fill(); // 重新渲染 + } + } + } + + // 恢复焦点(点击后回到原输入框) + if (this.picker.is(':visible') && this._focused_from) { + this._focused_from.focus(); + } + delete this._focused_from; + }, + + /** + * 处理日期单元格点击 + * @param {Event} e 点击事件 + */ + dayCellClick: function(e) { + var $target = $(e.currentTarget); + var timestamp = $target.data('date'); // 获取存储的时间戳 + var date = new Date(timestamp); // 转为日期对象 + + // 触发年月变更事件(如果配置了) + if (this.o.updateViewDate) { + if (date.getUTCFullYear() !== this.viewDate.getUTCFullYear()) { + this._trigger('changeYear', this.viewDate); + } + if (date.getUTCMonth() !== this.viewDate.getUTCMonth()) { + this._trigger('changeMonth', this.viewDate); + } + } + + this._setDate(date); // 选中日期 + }, + + /** + * 处理导航箭头点击(切换月份/年份) + * @param {Event} e 点击事件 + */ + navArrowsClick: function(e) { + var $target = $(e.currentTarget); + if ($target.hasClass('disabled')) return; // 禁用状态不处理 + + var dir = $target.hasClass('prev') ? -1 : 1; // 方向:左-1,右+1 + if (this.viewMode !== 0) { // 非日视图按跨度切换(如年视图一次10年) + dir *= DPGlobal.viewModes[this.viewMode].navStep * 12; + } + this.viewDate = this.moveMonth(this.viewDate, dir); // 移动月份 + this._trigger(DPGlobal.viewModes[this.viewMode].e, this.viewDate); // 触发事件 + this.fill(); // 重新渲染 + }, + + /** + * 切换多日期选择状态(添加/移除选中日期) + * @param {Date} date 日期 + */ + _toggle_multidate: function(date) { + var ix = this.dates.contains(date); // 检查是否已选中 + if (!date) { + this.dates.clear(); // 空日期则清空 + return; + } + + if (ix !== -1) { // 已选中,移除 + // 多日期模式或允许取消选中时才移除 + if (this.o.multidate === true || this.o.multidate > 1 || this.o.toggleActive) { + this.dates.remove(ix); + } + } else { // 未选中,添加 + if (this.o.multidate === false) { // 单选模式,先清空 + this.dates.clear(); + this.dates.push(date); + } else { // 多选模式 + this.dates.push(date); + // 限制数量(如最多选3个) + if (typeof this.o.multidate === 'number') { + while (this.dates.length > this.o.multidate) this.dates.remove(0); // 移除最早的 + } + } + } + }, + + /** + * 设置选中日期(内部方法) + * @param {Date} date 日期 + * @param {string} which 'date'=选中日期,'view'=仅更新视图 + */ + _setDate: function(date, which) { + if (!which || which === 'date') { + this._toggle_multidate(date && new Date(date)); // 切换选中状态 + } + if ((!which && this.o.updateViewDate) || which === 'view') { + this.viewDate = date && new Date(date); // 更新视图日期 + } + + this.fill(); // 重新渲染 + this.setValue(); // 更新输入框 + if (!which || which !== 'view') { + this._trigger('changeDate'); // 触发变更事件 + } + this.inputField.trigger('change'); // 触发输入框change事件 + if (this.o.autoclose && (!which || which === 'date')) { + this.hide(); // 自动关闭 + } + }, + + /** + * 日期加减天数 + * @param {Date} date 基准日期 + * @param {number} dir 方向(正数加,负数减) + * @returns {Date} 新日期 + */ + moveDay: function(date, dir) { + var newDate = new Date(date); + newDate.setUTCDate(date.getUTCDate() + dir); // UTC日期加天数 + return newDate; + }, + + /** + * 日期加减周数 + * @param {Date} date 基准日期 + * @param {number} dir 方向 + * @returns {Date} 新日期 + */ + moveWeek: function(date, dir) { + return this.moveDay(date, dir * 7); // 一周=7天 + }, + + /** + * 日期加减月份(处理月份边界,如 31 日跨月) + * 核心难点:处理不同月份天数不同的问题(如1月31日加1个月应为2月28/29日) + * @param {Date} date 基准日期 + * @param {number} dir 方向 + * @returns {Date} 新日期 + */ + moveMonth: function(date, dir) { + if (!isValidDate(date)) return this.o.defaultViewDate; // 无效日期返回默认 + if (!dir) return date; // 方向为0则返回原日期 + + var new_date = new Date(date.valueOf()), // 复制日期 + day = new_date.getUTCDate(), // 日期 + month = new_date.getUTCMonth(), // 月份 + mag = Math.abs(dir), // 绝对值 + new_month, test; + + dir = dir > 0 ? 1 : -1; // 标准化方向 + if (mag === 1) { + // 单月移动 + test = dir === -1 ? function() { // 后退一个月 + return new_date.getUTCMonth() === month; // 未成功后退则继续调整 + } : function() { // 前进一个月 + return new_date.getUTCMonth() !== new_month; // 未成功前进则继续调整 + }; + new_month = month + dir; + new_date.setUTCMonth(new_month); + new_month = (new_month + 12) % 12; // 处理 12/-1 边界(转为0-11) + } else { + // 多月移动(如+3个月) + for (var i = 0; i < mag; i++) new_date = this.moveMonth(new_date, dir); + new_month = new_date.getUTCMonth(); + new_date.setUTCDate(day); // 尝试设置原日期 + test = function() { return new_month !== new_date.getUTCMonth(); }; // 跨月则需要调整 + } + + // 处理月份天数不足的情况(如 1月31日 → 2月28日) + while (test()) { + new_date.setUTCDate(--day); // 日期减1 + new_date.setUTCMonth(new_month); // 重新设置月份 + } + return new_date; + }, + + /** + * 日期加减年份 + * @param {Date} date 基准日期 + * @param {number} dir 方向 + * @returns {Date} 新日期 + */ + moveYear: function(date, dir) { + return this.moveMonth(date, dir * 12); // 一年=12个月 + }, + + /** + * 移动到下一个可用日期(跳过禁用日期) + * 用于键盘导航,确保不会选中禁用日期 + * @param {Date} date 基准日期 + * @param {number} dir 方向 + * @param {string} fn 移动方法(moveDay/moveMonth 等) + * @returns {Date|false} 新日期或 false(无可用日期) + */ + moveAvailableDate: function(date, dir, fn) { + do { + date = this[fn](date, dir); // 按指定方法移动 + if (!this.dateWithinRange(date)) return false; // 超出范围返回false + fn = 'moveDay'; // 首次移动后按天迭代(确保不会跳过可用日期) + } while (this.dateIsDisabled(date)); // 跳过禁用日期 + return date; + }, + + /** + * 判断日期所在星期是否被禁用 + * @param {Date} date 日期 + * @returns {boolean} 是否禁用 + */ + weekOfDateIsDisabled: function(date) { + return $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1; + }, + + /** + * 判断日期是否被禁用(禁用星期或禁用日期列表) + * @param {Date} date 日期 + * @returns {boolean} 是否禁用 + */ + dateIsDisabled: function(date) { + return ( + this.weekOfDateIsDisabled(date) || // 星期被禁用 + // 日期在禁用列表中 + $.grep(this.o.datesDisabled, function(d) { + return isUTCEquals(date, d); + }).length > 0 + ); + }, + + /** + * 判断日期是否在有效范围内(startDate <= date <= endDate) + * @param {Date} date 日期 + * @returns {boolean} 是否在范围内 + */ + dateWithinRange: function(date) { + return date >= this.o.startDate && date <= this.o.endDate; + }, + + /** + * 处理键盘事件(导航、选择日期) + * 支持方向键、回车、ESC等操作 + * @param {Event} e 键盘事件 + */ + keydown: function(e) { + if (!this.picker.is(':visible')) { + // 未显示时,下箭头或ESC显示日历 + if (e.keyCode === 40 || e.keyCode === 27) { + this.show(); + e.stopPropagation(); + } + return; + } + + var dateChanged = false, // 日期是否变更 + dir, newViewDate, + focusDate = this.focusDate || this.viewDate; // 当前聚焦日期 + + switch (e.keyCode) { + case 27: // ESC:取消聚焦或隐藏 + if (this.focusDate) { + this.focusDate = null; // 清除聚焦 + this.viewDate = this.dates.get(-1) || this.viewDate; // 恢复视图日期 + this.fill(); // 重新渲染 + } else { + this.hide(); // 隐藏日历 + } + e.preventDefault(); + e.stopPropagation(); + break; + + case 37: // 左 + case 38: // 上 + case 39: // 右 + case 40: // 下 + if (!this.o.keyboardNavigation || this.o.daysOfWeekDisabled.length === 7) break; // 禁用键盘导航或全周禁用 + + dir = e.keyCode === 37 || e.keyCode === 38 ? -1 : 1; // 左/上为-1,右/下为1 + if (this.viewMode === 0) { // 日视图导航 + if (e.ctrlKey) { // Ctrl+方向:加减年 + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveYear'); + if (newViewDate) this._trigger('changeYear', this.viewDate); + } else if (e.shiftKey) { // Shift+方向:加减月 + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveMonth'); + if (newViewDate) this._trigger('changeMonth', this.viewDate); + } else if (e.keyCode === 37 || e.keyCode === 39) { // 左右:加减天 + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveDay'); + } else if (!this.weekOfDateIsDisabled(focusDate)) { // 上下:加减周(当前星期不禁用) + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveWeek'); + } + } else if (this.viewMode === 1) { // 月视图导航 + if (e.keyCode === 38 || e.keyCode === 40) dir *= 4; // 上下键跨4个月 + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveMonth'); + } else if (this.viewMode === 2) { // 年视图导航 + if (e.keyCode === 38 || e.keyCode === 40) dir *= 4; // 上下键跨4年 + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveYear'); + } + + if (newViewDate) { // 更新聚焦日期 + this.focusDate = this.viewDate = newViewDate; + this.setValue(); // 更新输入框 + this.fill(); // 重新渲染 + e.preventDefault(); // 阻止默认滚动 + } + break; + + case 13: // 回车:选中聚焦日期 + if (!this.o.forceParse) break; + focusDate = this.focusDate || this.dates.get(-1) || this.viewDate; + if (this.o.keyboardNavigation) { + this._toggle_multidate(focusDate); // 切换选中状态 + dateChanged = true; + } + this.focusDate = null; // 清除聚焦 + this.viewDate = this.dates.get(-1) || this.viewDate; // 恢复视图日期 + this.setValue(); // 更新输入框 + this.fill(); // 重新渲染 + if (this.picker.is(':visible')) { + e.preventDefault(); + e.stopPropagation(); + if (this.o.autoclose) this.hide(); // 自动关闭 + } + break; + + case 9: // Tab:隐藏日历 + this.focusDate = null; + this.viewDate = this.dates.get(-1) || this.viewDate; + this.fill(); + this.hide(); + break; + } + + // 触发变更事件 + if (dateChanged) { + if (this.dates.length) this._trigger('changeDate'); + else this._trigger('clearDate'); + this.inputField.trigger('change'); + } + }, + + /** + * 设置视图模式(日/月/年/十年/世纪) + * @param {number} viewMode 视图模式索引 + */ + setViewMode: function(viewMode) { + this.viewMode = viewMode; + // 隐藏所有视图,显示当前视图 + this.picker.children('div').hide() + .filter('.datepicker-' + DPGlobal.viewModes[this.viewMode].clsName).show(); + this.updateNavArrows(); // 更新箭头状态 + this._trigger('changeViewMode', new Date(this.viewDate)); // 触发事件 } - } - return cls; - }, - - _fill_yearsView: function(selector, cssClass, factor, year, startYear, endYear, beforeFn){ - var html = ''; - var step = factor / 10; - var view = this.picker.find(selector); - var startVal = Math.floor(year / factor) * factor; - var endVal = startVal + step * 9; - var focusedVal = Math.floor(this.viewDate.getFullYear() / step) * step; - var selected = $.map(this.dates, function(d){ - return Math.floor(d.getUTCFullYear() / step) * step; - }); - - var classes, tooltip, before; - for (var currVal = startVal - step; currVal <= endVal + step; currVal += step) { - classes = [cssClass]; - tooltip = null; - - if (currVal === startVal - step) { - classes.push('old'); - } else if (currVal === endVal + step) { - classes.push('new'); - } - if ($.inArray(currVal, selected) !== -1) { - classes.push('active'); - } - if (currVal < startYear || currVal > endYear) { - classes.push('disabled'); - } - if (currVal === focusedVal) { - classes.push('focused'); + }; + + + // ------------------------------ + // DateRangePicker:日期范围选择器 + // ------------------------------ + + var DateRangePicker = function(element, options) { + $.data(element, 'datepicker', this); // 绑定实例到DOM + this.element = $(element); + // 范围选择的两个输入框(从配置获取) + this.inputs = $.map(options.inputs, function(i) { + return i.jquery ? i[0] : i; // 兼容jQuery对象和DOM元素 + }); + delete options.inputs; // 清除配置中的inputs + + this.keepEmptyValues = options.keepEmptyValues; // 是否保留空值 + delete options.keepEmptyValues; + + // 初始化两个日期选择器并绑定联动事件 + datepickerPlugin.call($(this.inputs), options) + .on('changeDate', $.proxy(this.dateUpdated, this)); // 日期变更时联动 + + // 获取两个Datepicker实例 + this.pickers = $.map(this.inputs, function(i) { + return $.data(i, 'datepicker'); + }); + this.updateDates(); // 初始化日期范围 + }; + + DateRangePicker.prototype = { + /** + * 更新日期范围 + */ + updateDates: function() { + // 获取两个选择器的UTC日期 + this.dates = $.map(this.pickers, function(i) { + return i.getUTCDate(); + }); + this.updateRanges(); // 更新范围样式 + }, + + /** + * 更新范围样式(高亮开始-结束之间的日期) + */ + updateRanges: function() { + // 转为时间戳数组 + var range = $.map(this.dates, function(d) { + return d.valueOf(); + }); + // 两个选择器都应用范围样式 + $.each(this.pickers, function(i, p) { + p.setRange(range); + }); + }, + + /** + * 日期变更时联动更新(确保开始 <= 结束) + * @param {Event} e 变更事件 + */ + dateUpdated: function(e) { + if (this.updating) return; // 避免递归调用 + this.updating = true; + + var dp = $.data(e.target, 'datepicker'); // 触发事件的选择器 + if (!dp) return; + + var new_date = dp.getUTCDate(), // 新日期 + i = $.inArray(e.target, this.inputs), // 当前输入框索引(0=开始,1=结束) + j = i - 1, k = i + 1, // 左右邻居索引 + l = this.inputs.length; // 输入框数量(通常为2) + + // 空值处理 + $.each(this.pickers, function(i, p) { + // 当前选择器或允许保留空值时,不自动填充 + if (!p.getUTCDate() && (p === dp || !this.keepEmptyValues)) + p.setUTCDate(new_date); // 空值则同步新日期 + }, this); + + // 确保开始 <= 结束 + if (new_date < this.dates[j]) { // 开始日期前移,更新左侧 + while (j >= 0 && new_date < this.dates[j]) { + this.pickers[j--].setUTCDate(new_date); // 左侧选择器同步新日期 + } + } else if (new_date > this.dates[k]) { // 结束日期后移,更新右侧 + while (k < l && new_date > this.dates[k]) { + this.pickers[k++].setUTCDate(new_date); // 右侧选择器同步新日期 + } + } + + this.updateDates(); // 更新范围 + delete this.updating; // 解除锁定 + }, + + /** + * 销毁范围选择器 + */ + destroy: function() { + $.map(this.pickers, function(p) { p.destroy(); }); // 销毁两个选择器 + $(this.inputs).off('changeDate', this.dateUpdated); // 解绑事件 + delete this.element.data().datepicker; // 清除数据 + }, + + // 兼容旧版本的别名 + remove: alias('destroy', 'Method `remove` is deprecated, use `destroy` instead') + }; + + + // ------------------------------ + // 插件初始化与配置 + // ------------------------------ + + /** + * 从 DOM data-* 属性读取配置 + * 支持 data-date-format、data-date-language 等属性 + * @param {Element} el DOM 元素 + * @param {string} prefix 前缀(如 'date') + * @returns {Object} 配置对象 + */ + function opts_from_el(el, prefix) { + var data = $(el).data(), out = {}, inkey, + replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'); // 匹配 data-date-xxx + for (var key in data) { + if (replace.test(key)) { + // 转换为驼峰式(如 dateFormat → format) + inkey = key.replace(replace, function(_, a) { return a.toLowerCase(); }); + out[inkey] = data[key]; + } } + return out; + } - if (beforeFn !== $.noop) { - before = beforeFn(new Date(currVal, 0, 1)); - if (before === undefined) { - before = {}; - } else if (typeof before === 'boolean') { - before = {enabled: before}; - } else if (typeof before === 'string') { - before = {classes: before}; - } - if (before.enabled === false) { - classes.push('disabled'); - } - if (before.classes) { - classes = classes.concat(before.classes.split(/\s+/)); - } - if (before.tooltip) { - tooltip = before.tooltip; - } - } - - html += '' + currVal + ''; - } - - view.find('.datepicker-switch').text(startVal + '-' + endVal); - view.find('td').html(html); - }, - - fill: function(){ - var d = new Date(this.viewDate), - year = d.getUTCFullYear(), - month = d.getUTCMonth(), - startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, - startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, - endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, - endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, - todaytxt = dates[this.o.language].today || dates['en'].today || '', - cleartxt = dates[this.o.language].clear || dates['en'].clear || '', - titleFormat = dates[this.o.language].titleFormat || dates['en'].titleFormat, - tooltip, - before; - if (isNaN(year) || isNaN(month)) - return; - this.picker.find('.datepicker-days .datepicker-switch') - .text(DPGlobal.formatDate(d, titleFormat, this.o.language)); - this.picker.find('tfoot .today') - .text(todaytxt) - .css('display', this.o.todayBtn === true || this.o.todayBtn === 'linked' ? 'table-cell' : 'none'); - this.picker.find('tfoot .clear') - .text(cleartxt) - .css('display', this.o.clearBtn === true ? 'table-cell' : 'none'); - this.picker.find('thead .datepicker-title') - .text(this.o.title) - .css('display', typeof this.o.title === 'string' && this.o.title !== '' ? 'table-cell' : 'none'); - this.updateNavArrows(); - this.fillMonths(); - var prevMonth = UTCDate(year, month, 0), - day = prevMonth.getUTCDate(); - prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7); - var nextMonth = new Date(prevMonth); - if (prevMonth.getUTCFullYear() < 100){ - nextMonth.setUTCFullYear(prevMonth.getUTCFullYear()); - } - nextMonth.setUTCDate(nextMonth.getUTCDate() + 42); - nextMonth = nextMonth.valueOf(); - var html = []; - var weekDay, clsName; - while (prevMonth.valueOf() < nextMonth){ - weekDay = prevMonth.getUTCDay(); - if (weekDay === this.o.weekStart){ - html.push(''); - if (this.o.calendarWeeks){ - // ISO 8601: First week contains first thursday. - // ISO also states week starts on Monday, but we can be more abstract here. - var - // Start of current week: based on weekstart/current date - ws = new Date(+prevMonth + (this.o.weekStart - weekDay - 7) % 7 * 864e5), - // Thursday of this week - th = new Date(Number(ws) + (7 + 4 - ws.getUTCDay()) % 7 * 864e5), - // First Thursday of year, year from thursday - yth = new Date(Number(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay()) % 7 * 864e5), - // Calendar week: ms between thursdays, div ms per day, div 7 days - calWeek = (th - yth) / 864e5 / 7 + 1; - html.push(''+ calWeek +''); - } - } - clsName = this.getClassNames(prevMonth); - clsName.push('day'); - - var content = prevMonth.getUTCDate(); - - if (this.o.beforeShowDay !== $.noop){ - before = this.o.beforeShowDay(this._utc_to_local(prevMonth)); - if (before === undefined) - before = {}; - else if (typeof before === 'boolean') - before = {enabled: before}; - else if (typeof before === 'string') - before = {classes: before}; - if (before.enabled === false) - clsName.push('disabled'); - if (before.classes) - clsName = clsName.concat(before.classes.split(/\s+/)); - if (before.tooltip) - tooltip = before.tooltip; - if (before.content) - content = before.content; - } - - //Check if uniqueSort exists (supported by jquery >=1.12 and >=2.2) - //Fallback to unique function for older jquery versions - if ($.isFunction($.uniqueSort)) { - clsName = $.uniqueSort(clsName); - } else { - clsName = $.unique(clsName); - } - - html.push('' + content + ''); - tooltip = null; - if (weekDay === this.o.weekEnd){ - html.push(''); - } - prevMonth.setUTCDate(prevMonth.getUTCDate() + 1); - } - this.picker.find('.datepicker-days tbody').html(html.join('')); - - var monthsTitle = dates[this.o.language].monthsTitle || dates['en'].monthsTitle || 'Months'; - var months = this.picker.find('.datepicker-months') - .find('.datepicker-switch') - .text(this.o.maxViewMode < 2 ? monthsTitle : year) - .end() - .find('tbody span').removeClass('active'); - - $.each(this.dates, function(i, d){ - if (d.getUTCFullYear() === year) - months.eq(d.getUTCMonth()).addClass('active'); - }); - - if (year < startYear || year > endYear){ - months.addClass('disabled'); - } - if (year === startYear){ - months.slice(0, startMonth).addClass('disabled'); - } - if (year === endYear){ - months.slice(endMonth+1).addClass('disabled'); - } - - if (this.o.beforeShowMonth !== $.noop){ - var that = this; - $.each(months, function(i, month){ - var moDate = new Date(year, i, 1); - var before = that.o.beforeShowMonth(moDate); - if (before === undefined) - before = {}; - else if (typeof before === 'boolean') - before = {enabled: before}; - else if (typeof before === 'string') - before = {classes: before}; - if (before.enabled === false && !$(month).hasClass('disabled')) - $(month).addClass('disabled'); - if (before.classes) - $(month).addClass(before.classes); - if (before.tooltip) - $(month).prop('title', before.tooltip); - }); - } - - // Generating decade/years picker - this._fill_yearsView( - '.datepicker-years', - 'year', - 10, - year, - startYear, - endYear, - this.o.beforeShowYear - ); - - // Generating century/decades picker - this._fill_yearsView( - '.datepicker-decades', - 'decade', - 100, - year, - startYear, - endYear, - this.o.beforeShowDecade - ); - - // Generating millennium/centuries picker - this._fill_yearsView( - '.datepicker-centuries', - 'century', - 1000, - year, - startYear, - endYear, - this.o.beforeShowCentury - ); - }, - - updateNavArrows: function(){ - if (!this._allow_update) - return; - - var d = new Date(this.viewDate), - year = d.getUTCFullYear(), - month = d.getUTCMonth(), - startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, - startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, - endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, - endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, - prevIsDisabled, - nextIsDisabled, - factor = 1; - switch (this.viewMode){ - case 0: - prevIsDisabled = year <= startYear && month <= startMonth; - nextIsDisabled = year >= endYear && month >= endMonth; - break; - case 4: - factor *= 10; - /* falls through */ - case 3: - factor *= 10; - /* falls through */ - case 2: - factor *= 10; - /* falls through */ - case 1: - prevIsDisabled = Math.floor(year / factor) * factor <= startYear; - nextIsDisabled = Math.floor(year / factor) * factor + factor >= endYear; - break; - } - - this.picker.find('.prev').toggleClass('disabled', prevIsDisabled); - this.picker.find('.next').toggleClass('disabled', nextIsDisabled); - }, - - click: function(e){ - e.preventDefault(); - e.stopPropagation(); - - var target, dir, day, year, month; - target = $(e.target); - - // Clicked on the switch - if (target.hasClass('datepicker-switch') && this.viewMode !== this.o.maxViewMode){ - this.setViewMode(this.viewMode + 1); - } - - // Clicked on today button - if (target.hasClass('today') && !target.hasClass('day')){ - this.setViewMode(0); - this._setDate(UTCToday(), this.o.todayBtn === 'linked' ? null : 'view'); - } - - // Clicked on clear button - if (target.hasClass('clear')){ - this.clearDates(); - } - - if (!target.hasClass('disabled')){ - // Clicked on a month, year, decade, century - if (target.hasClass('month') - || target.hasClass('year') - || target.hasClass('decade') - || target.hasClass('century')) { - this.viewDate.setUTCDate(1); - - day = 1; - if (this.viewMode === 1){ - month = target.parent().find('span').index(target); - year = this.viewDate.getUTCFullYear(); - this.viewDate.setUTCMonth(month); - } else { - month = 0; - year = Number(target.text()); - this.viewDate.setUTCFullYear(year); - } - - this._trigger(DPGlobal.viewModes[this.viewMode - 1].e, this.viewDate); - - if (this.viewMode === this.o.minViewMode){ - this._setDate(UTCDate(year, month, day)); - } else { - this.setViewMode(this.viewMode - 1); - this.fill(); - } - } - } - - if (this.picker.is(':visible') && this._focused_from){ - this._focused_from.focus(); - } - delete this._focused_from; - }, - - dayCellClick: function(e){ - var $target = $(e.currentTarget); - var timestamp = $target.data('date'); - var date = new Date(timestamp); - - if (this.o.updateViewDate) { - if (date.getUTCFullYear() !== this.viewDate.getUTCFullYear()) { - this._trigger('changeYear', this.viewDate); - } - - if (date.getUTCMonth() !== this.viewDate.getUTCMonth()) { - this._trigger('changeMonth', this.viewDate); - } - } - this._setDate(date); - }, - - // Clicked on prev or next - navArrowsClick: function(e){ - var $target = $(e.currentTarget); - var dir = $target.hasClass('prev') ? -1 : 1; - if (this.viewMode !== 0){ - dir *= DPGlobal.viewModes[this.viewMode].navStep * 12; - } - this.viewDate = this.moveMonth(this.viewDate, dir); - this._trigger(DPGlobal.viewModes[this.viewMode].e, this.viewDate); - this.fill(); - }, - - _toggle_multidate: function(date){ - var ix = this.dates.contains(date); - if (!date){ - this.dates.clear(); - } - - if (ix !== -1){ - if (this.o.multidate === true || this.o.multidate > 1 || this.o.toggleActive){ - this.dates.remove(ix); - } - } else if (this.o.multidate === false) { - this.dates.clear(); - this.dates.push(date); - } - else { - this.dates.push(date); - } - - if (typeof this.o.multidate === 'number') - while (this.dates.length > this.o.multidate) - this.dates.remove(0); - }, - - _setDate: function(date, which){ - if (!which || which === 'date') - this._toggle_multidate(date && new Date(date)); - if ((!which && this.o.updateViewDate) || which === 'view') - this.viewDate = date && new Date(date); - - this.fill(); - this.setValue(); - if (!which || which !== 'view') { - this._trigger('changeDate'); - } - this.inputField.trigger('change'); - if (this.o.autoclose && (!which || which === 'date')){ - this.hide(); - } - }, - - moveDay: function(date, dir){ - var newDate = new Date(date); - newDate.setUTCDate(date.getUTCDate() + dir); - - return newDate; - }, - - moveWeek: function(date, dir){ - return this.moveDay(date, dir * 7); - }, - - moveMonth: function(date, dir){ - if (!isValidDate(date)) - return this.o.defaultViewDate; - if (!dir) - return date; - var new_date = new Date(date.valueOf()), - day = new_date.getUTCDate(), - month = new_date.getUTCMonth(), - mag = Math.abs(dir), - new_month, test; - dir = dir > 0 ? 1 : -1; - if (mag === 1){ - test = dir === -1 - // If going back one month, make sure month is not current month - // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02) - ? function(){ - return new_date.getUTCMonth() === month; - } - // If going forward one month, make sure month is as expected - // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02) - : function(){ - return new_date.getUTCMonth() !== new_month; - }; - new_month = month + dir; - new_date.setUTCMonth(new_month); - // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11 - new_month = (new_month + 12) % 12; - } - else { - // For magnitudes >1, move one month at a time... - for (var i=0; i < mag; i++) - // ...which might decrease the day (eg, Jan 31 to Feb 28, etc)... - new_date = this.moveMonth(new_date, dir); - // ...then reset the day, keeping it in the new month - new_month = new_date.getUTCMonth(); - new_date.setUTCDate(day); - test = function(){ - return new_month !== new_date.getUTCMonth(); - }; - } - // Common date-resetting loop -- if date is beyond end of month, make it - // end of month - while (test()){ - new_date.setUTCDate(--day); - new_date.setUTCMonth(new_month); - } - return new_date; - }, - - moveYear: function(date, dir){ - return this.moveMonth(date, dir*12); - }, - - moveAvailableDate: function(date, dir, fn){ - do { - date = this[fn](date, dir); - - if (!this.dateWithinRange(date)) - return false; - - fn = 'moveDay'; - } - while (this.dateIsDisabled(date)); - - return date; - }, - - weekOfDateIsDisabled: function(date){ - return $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1; - }, - - dateIsDisabled: function(date){ - return ( - this.weekOfDateIsDisabled(date) || - $.grep(this.o.datesDisabled, function(d){ - return isUTCEquals(date, d); - }).length > 0 - ); - }, - - dateWithinRange: function(date){ - return date >= this.o.startDate && date <= this.o.endDate; - }, - - keydown: function(e){ - if (!this.picker.is(':visible')){ - if (e.keyCode === 40 || e.keyCode === 27) { // allow down to re-show picker - this.show(); - e.stopPropagation(); + /** + * 从语言配置读取默认值 + * 如语言包中定义的 format、weekStart 等 + * @param {string} lang 语言代码 + * @returns {Object} 配置对象 + */ + function opts_from_locale(lang) { + if (!dates[lang]) { + lang = lang.split('-')[0]; // 降级到主语言 + if (!dates[lang]) return; } - return; - } - var dateChanged = false, - dir, newViewDate, - focusDate = this.focusDate || this.viewDate; - switch (e.keyCode){ - case 27: // escape - if (this.focusDate){ - this.focusDate = null; - this.viewDate = this.dates.get(-1) || this.viewDate; - this.fill(); - } - else - this.hide(); - e.preventDefault(); - e.stopPropagation(); - break; - case 37: // left - case 38: // up - case 39: // right - case 40: // down - if (!this.o.keyboardNavigation || this.o.daysOfWeekDisabled.length === 7) - break; - dir = e.keyCode === 37 || e.keyCode === 38 ? -1 : 1; - if (this.viewMode === 0) { - if (e.ctrlKey){ - newViewDate = this.moveAvailableDate(focusDate, dir, 'moveYear'); - - if (newViewDate) - this._trigger('changeYear', this.viewDate); - } else if (e.shiftKey){ - newViewDate = this.moveAvailableDate(focusDate, dir, 'moveMonth'); - - if (newViewDate) - this._trigger('changeMonth', this.viewDate); - } else if (e.keyCode === 37 || e.keyCode === 39){ - newViewDate = this.moveAvailableDate(focusDate, dir, 'moveDay'); - } else if (!this.weekOfDateIsDisabled(focusDate)){ - newViewDate = this.moveAvailableDate(focusDate, dir, 'moveWeek'); - } - } else if (this.viewMode === 1) { - if (e.keyCode === 38 || e.keyCode === 40) { - dir = dir * 4; + var out = {}, d = dates[lang]; + // 仅提取需要合并的配置项 + $.each(locale_opts, function(i, k) { + if (k in d) out[k] = d[k]; + }); + return out; + } + + // 保存旧的 datepicker 方法(避免覆盖其他插件) + var old = $.fn.datepicker; + + /** + * jQuery 插件入口 + * 支持 $(el).datepicker(options) 初始化,或 $(el).datepicker('method') 调用方法 + * @param {string|Object} option 方法名或配置对象 + * @returns {jQuery|*} jQuery 对象或方法返回值 + */ + var datepickerPlugin = function(option) { + var args = Array.apply(null, arguments); + args.shift(); // 移除第一个参数(option) + var internal_return; // 方法返回值 + + this.each(function() { + var $this = $(this), + data = $this.data('datepicker'), + options = typeof option === 'object' && option; // 配置对象 + + if (!data) { // 未初始化,创建实例 + var elopts = opts_from_el(this, 'date'), // data-* 配置 + xopts = $.extend({}, defaults, elopts, options), // 临时合并 + locopts = opts_from_locale(xopts.language), // 语言默认配置 + // 最终合并:默认配置 < 语言配置 < data-* < 用户配置 + opts = $.extend({}, defaults, locopts, elopts, options); + + // 判断是单个选择器还是范围选择器 + if ($this.hasClass('input-daterange') || opts.inputs) { + opts.inputs = opts.inputs || $this.find('input').toArray(); // 范围选择的输入框 + data = new DateRangePicker(this, opts); + } else { + data = new Datepicker(this, opts); + } + $this.data('datepicker', data); // 绑定实例 } - newViewDate = this.moveAvailableDate(focusDate, dir, 'moveMonth'); - } else if (this.viewMode === 2) { - if (e.keyCode === 38 || e.keyCode === 40) { - dir = dir * 4; + + // 调用实例方法(如 $(el).datepicker('show')) + if (typeof option === 'string' && typeof data[option] === 'function') { + internal_return = data[option].apply(data, args); } - newViewDate = this.moveAvailableDate(focusDate, dir, 'moveYear'); - } - if (newViewDate){ - this.focusDate = this.viewDate = newViewDate; - this.setValue(); - this.fill(); - e.preventDefault(); - } - break; - case 13: // enter - if (!this.o.forceParse) - break; - focusDate = this.focusDate || this.dates.get(-1) || this.viewDate; - if (this.o.keyboardNavigation) { - this._toggle_multidate(focusDate); - dateChanged = true; - } - this.focusDate = null; - this.viewDate = this.dates.get(-1) || this.viewDate; - this.setValue(); - this.fill(); - if (this.picker.is(':visible')){ - e.preventDefault(); - e.stopPropagation(); - if (this.o.autoclose) - this.hide(); - } - break; - case 9: // tab - this.focusDate = null; - this.viewDate = this.dates.get(-1) || this.viewDate; - this.fill(); - this.hide(); - break; - } - if (dateChanged){ - if (this.dates.length) - this._trigger('changeDate'); - else - this._trigger('clearDate'); - this.inputField.trigger('change'); - } - }, - - setViewMode: function(viewMode){ - this.viewMode = viewMode; - this.picker - .children('div') - .hide() - .filter('.datepicker-' + DPGlobal.viewModes[this.viewMode].clsName) - .show(); - this.updateNavArrows(); - this._trigger('changeViewMode', new Date(this.viewDate)); - } - }; - - var DateRangePicker = function(element, options){ - $.data(element, 'datepicker', this); - this.element = $(element); - this.inputs = $.map(options.inputs, function(i){ - return i.jquery ? i[0] : i; - }); - delete options.inputs; - - this.keepEmptyValues = options.keepEmptyValues; - delete options.keepEmptyValues; - - datepickerPlugin.call($(this.inputs), options) - .on('changeDate', $.proxy(this.dateUpdated, this)); - - this.pickers = $.map(this.inputs, function(i){ - return $.data(i, 'datepicker'); - }); - this.updateDates(); - }; - DateRangePicker.prototype = { - updateDates: function(){ - this.dates = $.map(this.pickers, function(i){ - return i.getUTCDate(); - }); - this.updateRanges(); - }, - updateRanges: function(){ - var range = $.map(this.dates, function(d){ - return d.valueOf(); - }); - $.each(this.pickers, function(i, p){ - p.setRange(range); - }); - }, - dateUpdated: function(e){ - // `this.updating` is a workaround for preventing infinite recursion - // between `changeDate` triggering and `setUTCDate` calling. Until - // there is a better mechanism. - if (this.updating) - return; - this.updating = true; - - var dp = $.data(e.target, 'datepicker'); - - if (dp === undefined) { - return; - } - - var new_date = dp.getUTCDate(), - keep_empty_values = this.keepEmptyValues, - i = $.inArray(e.target, this.inputs), - j = i - 1, - k = i + 1, - l = this.inputs.length; - if (i === -1) - return; - - $.each(this.pickers, function(i, p){ - if (!p.getUTCDate() && (p === dp || !keep_empty_values)) - p.setUTCDate(new_date); - }); - - if (new_date < this.dates[j]){ - // Date being moved earlier/left - while (j >= 0 && new_date < this.dates[j]){ - this.pickers[j--].setUTCDate(new_date); - } - } else if (new_date > this.dates[k]){ - // Date being moved later/right - while (k < l && new_date > this.dates[k]){ - this.pickers[k++].setUTCDate(new_date); - } - } - this.updateDates(); - - delete this.updating; - }, - destroy: function(){ - $.map(this.pickers, function(p){ p.destroy(); }); - $(this.inputs).off('changeDate', this.dateUpdated); - delete this.element.data().datepicker; - }, - remove: alias('destroy', 'Method `remove` is deprecated and will be removed in version 2.0. Use `destroy` instead') - }; - - function opts_from_el(el, prefix){ - // Derive options from element data-attrs - var data = $(el).data(), - out = {}, inkey, - replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'); - prefix = new RegExp('^' + prefix.toLowerCase()); - function re_lower(_,a){ - return a.toLowerCase(); - } - for (var key in data) - if (prefix.test(key)){ - inkey = key.replace(replace, re_lower); - out[inkey] = data[key]; - } - return out; - } - - function opts_from_locale(lang){ - // Derive options from locale plugins - var out = {}; - // Check if "de-DE" style date is available, if not language should - // fallback to 2 letter code eg "de" - if (!dates[lang]){ - lang = lang.split('-')[0]; - if (!dates[lang]) - return; - } - var d = dates[lang]; - $.each(locale_opts, function(i,k){ - if (k in d) - out[k] = d[k]; - }); - return out; - } - - var old = $.fn.datepicker; - var datepickerPlugin = function(option){ - var args = Array.apply(null, arguments); - args.shift(); - var internal_return; - this.each(function(){ - var $this = $(this), - data = $this.data('datepicker'), - options = typeof option === 'object' && option; - if (!data){ - var elopts = opts_from_el(this, 'date'), - // Preliminary otions - xopts = $.extend({}, defaults, elopts, options), - locopts = opts_from_locale(xopts.language), - // Options priority: js args, data-attrs, locales, defaults - opts = $.extend({}, defaults, locopts, elopts, options); - if ($this.hasClass('input-daterange') || opts.inputs){ - $.extend(opts, { - inputs: opts.inputs || $this.find('input').toArray() - }); - data = new DateRangePicker(this, opts); - } - else { - data = new Datepicker(this, opts); - } - $this.data('datepicker', data); - } - if (typeof option === 'string' && typeof data[option] === 'function'){ - internal_return = data[option].apply(data, args); - } - }); - - if ( - internal_return === undefined || - internal_return instanceof Datepicker || - internal_return instanceof DateRangePicker - ) - return this; - - if (this.length > 1) - throw new Error('Using only allowed for the collection of a single element (' + option + ' function)'); - else - return internal_return; - }; - $.fn.datepicker = datepickerPlugin; - - var defaults = $.fn.datepicker.defaults = { - assumeNearbyYear: false, - autoclose: false, - beforeShowDay: $.noop, - beforeShowMonth: $.noop, - beforeShowYear: $.noop, - beforeShowDecade: $.noop, - beforeShowCentury: $.noop, - calendarWeeks: false, - clearBtn: false, - toggleActive: false, - daysOfWeekDisabled: [], - daysOfWeekHighlighted: [], - datesDisabled: [], - endDate: Infinity, - forceParse: true, - format: 'mm/dd/yyyy', - keepEmptyValues: false, - keyboardNavigation: true, - language: 'en', - minViewMode: 0, - maxViewMode: 4, - multidate: false, - multidateSeparator: ',', - orientation: "auto", - rtl: false, - startDate: -Infinity, - startView: 0, - todayBtn: false, - todayHighlight: false, - updateViewDate: true, - weekStart: 0, - disableTouchKeyboard: false, - enableOnReadonly: true, - showOnFocus: true, - zIndexOffset: 10, - container: 'body', - immediateUpdates: false, - title: '', - templates: { - leftArrow: '«', - rightArrow: '»' - }, - showWeekDays: true - }; - var locale_opts = $.fn.datepicker.locale_opts = [ - 'format', - 'rtl', - 'weekStart' - ]; - $.fn.datepicker.Constructor = Datepicker; - var dates = $.fn.datepicker.dates = { - en: { - days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], - daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], - daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], - months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], - monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], - today: "Today", - clear: "Clear", - titleFormat: "MM yyyy" - } - }; - - var DPGlobal = { - viewModes: [ - { - names: ['days', 'month'], - clsName: 'days', - e: 'changeMonth' - }, - { - names: ['months', 'year'], - clsName: 'months', - e: 'changeYear', - navStep: 1 - }, - { - names: ['years', 'decade'], - clsName: 'years', - e: 'changeDecade', - navStep: 10 - }, - { - names: ['decades', 'century'], - clsName: 'decades', - e: 'changeCentury', - navStep: 100 - }, - { - names: ['centuries', 'millennium'], - clsName: 'centuries', - e: 'changeMillennium', - navStep: 1000 - } - ], - validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g, - nonpunctuation: /[^ -\/:-@\u5e74\u6708\u65e5\[-`{-~\t\n\r]+/g, - parseFormat: function(format){ - if (typeof format.toValue === 'function' && typeof format.toDisplay === 'function') + }); + + // 支持链式调用或返回方法结果 + if (internal_return === undefined || internal_return instanceof Datepicker || internal_return instanceof DateRangePicker) { + return this; + } else { + if (this.length > 1) throw new Error('方法仅支持单个元素调用'); + return internal_return; + } + }; + + // 暴露插件到 jQuery + $.fn.datepicker = datepickerPlugin; + + // 默认配置(所有可配置项的默认值) + var defaults = $.fn.datepicker.defaults = { + assumeNearbyYear: false, // 两位数年份是否自动补全(如 24 → 2024) + autoclose: false, // 选中后自动关闭 + beforeShowDay: $.noop, // 日期单元格渲染前回调 + beforeShowMonth: $.noop, // 月份渲染前回调 + beforeShowYear: $.noop, // 年份渲染前回调 + beforeShowDecade: $.noop, // 十年渲染前回调 + beforeShowCentury: $.noop, // 世纪渲染前回调 + calendarWeeks: false, // 是否显示周数 + clearBtn: false, // 是否显示清空按钮 + toggleActive: false, // 是否允许取消选中 + daysOfWeekDisabled: [], // 禁用的星期(0=周日) + daysOfWeekHighlighted: [], // 高亮的星期 + datesDisabled: [], // 禁用的具体日期 + endDate: Infinity, // 结束日期限制 + forceParse: true, // 是否强制解析输入框内容 + format: 'mm/dd/yyyy', // 日期格式 + keepEmptyValues: false, // 范围选择器是否保留空值 + keyboardNavigation: true, // 是否支持键盘导航 + language: 'en', // 语言 + minViewMode: 0, // 最小视图模式(0=日) + maxViewMode: 4, // 最大视图模式(4=世纪) + multidate: false, // 是否支持多日期选择 + multidateSeparator: ',', // 多日期分隔符 + orientation: "auto", // 定位方式 + rtl: false, // 是否从右到左 + startDate: -Infinity, // 开始日期限制 + startView: 0, // 初始视图模式 + todayBtn: false, // 是否显示今天按钮 + todayHighlight: false, // 是否高亮今天 + updateViewDate: true, // 是否更新视图日期 + weekStart: 0, // 星期起始日(0=周日) + disableTouchKeyboard: false, // 触摸设备是否禁用虚拟键盘 + enableOnReadonly: true, // 只读输入框是否启用 + showOnFocus: true, // 聚焦时是否显示 + zIndexOffset: 10, // z-index 偏移量 + container: 'body', // 日历容器 + immediateUpdates: false, // 年月变化时是否立即更新 + title: '', // 自定义标题 + templates: { leftArrow: '«', rightArrow: '»' }, // 箭头模板 + showWeekDays: true // 是否显示星期标题 + }; + + // 语言配置中需要合并到默认配置的字段 + var locale_opts = $.fn.datepicker.locale_opts = ['format', 'rtl', 'weekStart']; + + // 暴露构造函数 + $.fn.datepicker.Constructor = Datepicker; + + // 语言包(默认英语) + var dates = $.fn.datepicker.dates = { + en: { + days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], + daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], + months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + today: "Today", + clear: "Clear", + titleFormat: "MM yyyy" // 标题格式(如 "June 2024") + } + }; + + + // ------------------------------ + // 全局常量:视图配置与模板 + // ------------------------------ + + var DPGlobal = { + // 视图模式配置(索引对应视图等级) + viewModes: [ + { names: ['days', 'month'], clsName: 'days', e: 'changeMonth' }, // 日视图 + { names: ['months', 'year'], clsName: 'months', e: 'changeYear', navStep: 1 }, // 月视图 + { names: ['years', 'decade'], clsName: 'years', e: 'changeDecade', navStep: 10 }, // 年视图 + { names: ['decades', 'century'], clsName: 'decades', e: 'changeCentury', navStep: 100 }, // 十年视图 + { names: ['centuries', 'millennium'], clsName: 'centuries', e: 'changeMillennium', navStep: 1000 } // 世纪视图 + ], + validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g, // 日期格式合法占位符(如 dd, MM, yyyy) + nonpunctuation: /[^ -\/:-@\u5e74\u6708\u65e5\[-`{-~\t\n\r]+/g, // 提取日期字符(排除标点) + + /** + * 解析日期格式(如 'yyyy-mm-dd' → { separators: ['-', '-'], parts: ['yyyy', 'mm', 'dd'] }) + * @param {string|Object} format 日期格式 + * @returns {Object} 解析后的格式对象 + */ + parseFormat: function(format) { + // 支持自定义格式化函数({ toValue: ..., toDisplay: ... }) + if (typeof format.toValue === 'function' && typeof format.toDisplay === 'function') return format; - // IE treats \0 as a string end in inputs (truncating the value), - // so it's a bad format delimiter, anyway - var separators = format.replace(this.validParts, '\0').split('\0'), - parts = format.match(this.validParts); - if (!separators || !separators.length || !parts || parts.length === 0){ - throw new Error("Invalid date format."); - } - return {separators: separators, parts: parts}; - }, - parseDate: function(date, format, language, assumeNearby){ - if (!date) - return undefined; - if (date instanceof Date) - return date; - if (typeof format === 'string') - format = DPGlobal.parseFormat(format); - if (format.toValue) - return format.toValue(date, format, language); - var fn_map = { - d: 'moveDay', - m: 'moveMonth', - w: 'moveWeek', - y: 'moveYear' - }, - dateAliases = { - yesterday: '-1d', - today: '+0d', - tomorrow: '+1d' - }, - parts, part, dir, i, fn; - if (date in dateAliases){ - date = dateAliases[date]; - } - if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/i.test(date)){ - parts = date.match(/([\-+]\d+)([dmwy])/gi); - date = new Date(); - for (i=0; i < parts.length; i++){ - part = parts[i].match(/([\-+]\d+)([dmwy])/i); - dir = Number(part[1]); - fn = fn_map[part[2].toLowerCase()]; - date = Datepicker.prototype[fn](date, dir); - } - return Datepicker.prototype._zero_utc_time(date); - } - - parts = date && date.match(this.nonpunctuation) || []; - - function applyNearbyYear(year, threshold){ - if (threshold === true) - threshold = 10; - - // if year is 2 digits or less, than the user most likely is trying to get a recent century - if (year < 100){ - year += 2000; - // if the new year is more than threshold years in advance, use last century - if (year > ((new Date()).getFullYear()+threshold)){ - year -= 100; - } - } - - return year; - } - - var parsed = {}, - setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'], - setters_map = { - yyyy: function(d,v){ - return d.setUTCFullYear(assumeNearby ? applyNearbyYear(v, assumeNearby) : v); - }, - m: function(d,v){ - if (isNaN(d)) - return d; - v -= 1; - while (v < 0) v += 12; - v %= 12; - d.setUTCMonth(v); - while (d.getUTCMonth() !== v) - d.setUTCDate(d.getUTCDate()-1); - return d; - }, - d: function(d,v){ - return d.setUTCDate(v); - } - }, - val, filtered; - setters_map['yy'] = setters_map['yyyy']; - setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m']; - setters_map['dd'] = setters_map['d']; - date = UTCToday(); - var fparts = format.parts.slice(); - // Remove noop parts - if (parts.length !== fparts.length){ - fparts = $(fparts).filter(function(i,p){ - return $.inArray(p, setters_order) !== -1; - }).toArray(); - } - // Process remainder - function match_part(){ - var m = this.slice(0, parts[i].length), - p = parts[i].slice(0, m.length); - return m.toLowerCase() === p.toLowerCase(); - } - if (parts.length === fparts.length){ - var cnt; - for (i=0, cnt = fparts.length; i < cnt; i++){ - val = parseInt(parts[i], 10); - part = fparts[i]; - if (isNaN(val)){ - switch (part){ - case 'MM': - filtered = $(dates[language].months).filter(match_part); - val = $.inArray(filtered[0], dates[language].months) + 1; - break; - case 'M': - filtered = $(dates[language].monthsShort).filter(match_part); - val = $.inArray(filtered[0], dates[language].monthsShort) + 1; - break; - } - } - parsed[part] = val; - } - var _date, s; - for (i=0; i < setters_order.length; i++){ - s = setters_order[i]; - if (s in parsed && !isNaN(parsed[s])){ - _date = new Date(date); - setters_map[s](_date, parsed[s]); - if (!isNaN(_date)) - date = _date; - } - } - } - return date; - }, - formatDate: function(date, format, language){ - if (!date) - return ''; - if (typeof format === 'string') - format = DPGlobal.parseFormat(format); - if (format.toDisplay) - return format.toDisplay(date, format, language); + // 提取分隔符和格式部分 + var separators = format.replace(this.validParts, '\0').split('\0'), + parts = format.match(this.validParts); + if (!separators || !parts || parts.length === 0) + throw new Error("Invalid date format."); // 无效格式 + return { separators: separators, parts: parts }; + }, + + /** + * 解析日期字符串为 Date 对象 + * 支持绝对日期(如 '2024-06-01')和相对日期(如 'today', '+1d') + * @param {string|Date} date 日期字符串或对象 + * @param {Object} format 格式对象 + * @param {string} language 语言 + * @param {boolean|number} assumeNearby 是否自动补全年份 + * @returns {Date|undefined} 解析后的日期 + */ + parseDate: function(date, format, language, assumeNearby) { + if (!date) return undefined; + if (date instanceof Date) return date; // 已为日期对象 + if (typeof format === 'string') format = DPGlobal.parseFormat(format); + if (format.toValue) return format.toValue(date, format, language); // 自定义解析 + + // 支持相对日期(如 'today' → 今天,'+1d' → 明天) + var fn_map = { d: 'moveDay', m: 'moveMonth', w: 'moveWeek', y: 'moveYear' }, + dateAliases = { yesterday: '-1d', today: '+0d', tomorrow: '+1d' }; + if (date in dateAliases) date = dateAliases[date]; // 替换别名 + + // 解析相对日期(如 '+2m' → 加 2 个月) + if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/i.test(date)) { + var parts = date.match(/([\-+]\d+)([dmwy])/gi), // 提取每个部分(如 ['+2m', '-1w']) + result = new Date(); // 基准日期为今天 + for (var i = 0; i < parts.length; i++) { + var part = parts[i].match(/([\-+]\d+)([dmwy])/i); // 提取数值和单位 + result = Datepicker.prototype[fn_map[part[2].toLowerCase()]](result, Number(part[1])); + } + return Datepicker.prototype._zero_utc_time(result); // 时间归零 + } + + // 解析绝对日期(如 '2024-06-01') + var parts = date && date.match(this.nonpunctuation) || [], parsed = {}; // 提取非标点部分 + var setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'], // 解析顺序(年 → 月 → 日) + setters_map = { // 日期设置函数 + yyyy: function(d, v) { d.setUTCFullYear(assumeNearby ? applyNearbyYear(v, assumeNearby) : v); }, + m: function(d, v) { // 处理月份(1-12 → 0-11) + v -= 1; while (v < 0) v += 12; v %= 12; // 确保在0-11范围内 + d.setUTCMonth(v); + // 处理月份天数不足的情况(如3月31日 → 4月30日) + while (d.getUTCMonth() !== v) d.setUTCDate(d.getUTCDate() - 1); + }, + d: function(d, v) { d.setUTCDate(v); } // 设置日期 + }; + // 扩展映射(支持多种格式) + setters_map['yy'] = setters_map['yyyy']; + setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m']; + setters_map['dd'] = setters_map['d']; + + // 补全年份(如 24 → 2024,99 → 1999) + function applyNearbyYear(year, threshold) { + if (threshold === true) threshold = 10; // 默认阈值10年 + if (year < 100) { // 两位数年份 + year += 2000; // 先假设为20xx年 + // 如果超过当前年份+阈值,则视为19xx年 + if (year > (new Date()).getFullYear() + threshold) year -= 100; + } + return year; + } + + // 过滤有效的格式部分(仅保留年月日相关) + var fparts = format.parts.slice(); + fparts = $(fparts).filter(function(i, p) { return $.inArray(p, setters_order) !== -1; }).toArray(); + + // 格式部分与解析部分数量匹配时才解析 + if (parts.length === fparts.length) { + for (var i = 0; i < fparts.length; i++) { + var val = parseInt(parts[i], 10), part = fparts[i]; + if (isNaN(val)) { // 解析月份名称(如 'Jan' → 1) + switch (part) { + case 'MM': val = $.inArray(parts[i], dates[language].months) + 1; break; // 全称 + case 'M': val = $.inArray(parts[i], dates[language].monthsShort) + 1; break; // 缩写 + } + } + parsed[part] = val; // 存储解析结果 + } + + var date = UTCToday(); // 基准日期为今天 + // 按顺序设置年、月、日(确保覆盖优先级) + for (var i = 0; i < setters_order.length; i++) { + var s = setters_order[i]; + if (s in parsed && !isNaN(parsed[s])) { + var _date = new Date(date); // 复制当前日期 + setters_map[s](_date, parsed[s]); // 应用设置 + if (!isNaN(_date)) date = _date; // 有效则更新 + } + } + return date; + } + }, + + /** + * 格式化日期为字符串 + * @param {Date} date 日期 + * @param {Object} format 格式对象 + * @param {string} language 语言 + * @returns {string} 格式化后的字符串 + */ + formatDate: function(date, format, language) { + if (!date) return ''; + if (typeof format === 'string') format = DPGlobal.parseFormat(format); + if (format.toDisplay) return format.toDisplay(date, format, language); // 自定义格式化 + + // 提取日期各部分值 var val = { - d: date.getUTCDate(), - D: dates[language].daysShort[date.getUTCDay()], - DD: dates[language].days[date.getUTCDay()], - m: date.getUTCMonth() + 1, - M: dates[language].monthsShort[date.getUTCMonth()], - MM: dates[language].months[date.getUTCMonth()], - yy: date.getUTCFullYear().toString().substring(2), - yyyy: date.getUTCFullYear() - }; - val.dd = (val.d < 10 ? '0' : '') + val.d; - val.mm = (val.m < 10 ? '0' : '') + val.m; - date = []; - var seps = $.extend([], format.separators); - for (var i=0, cnt = format.parts.length; i <= cnt; i++){ - if (seps.length) - date.push(seps.shift()); - date.push(val[format.parts[i]]); - } - return date.join(''); - }, - headTemplate: ''+ - ''+ - ''+ - ''+ - ''+ - ''+defaults.templates.leftArrow+''+ - ''+ - ''+defaults.templates.rightArrow+''+ - ''+ - '', - contTemplate: '', - footTemplate: ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - '' - }; - DPGlobal.template = '
'+ - '
'+ - ''+ - DPGlobal.headTemplate+ - ''+ - DPGlobal.footTemplate+ - '
'+ - '
'+ - '
'+ - ''+ - DPGlobal.headTemplate+ - DPGlobal.contTemplate+ - DPGlobal.footTemplate+ - '
'+ - '
'+ - '
'+ - ''+ - DPGlobal.headTemplate+ - DPGlobal.contTemplate+ - DPGlobal.footTemplate+ - '
'+ - '
'+ - '
'+ - ''+ - DPGlobal.headTemplate+ - DPGlobal.contTemplate+ - DPGlobal.footTemplate+ - '
'+ - '
'+ - '
'+ - ''+ - DPGlobal.headTemplate+ - DPGlobal.contTemplate+ - DPGlobal.footTemplate+ - '
'+ - '
'+ - '
'; - - $.fn.datepicker.DPGlobal = DPGlobal; - - - /* DATEPICKER NO CONFLICT - * =================== */ - - $.fn.datepicker.noConflict = function(){ - $.fn.datepicker = old; - return this; - }; - - /* DATEPICKER VERSION - * =================== */ - $.fn.datepicker.version = '1.7.0'; - - $.fn.datepicker.deprecated = function(msg){ - var console = window.console; - if (console && console.warn) { - console.warn('DEPRECATED: ' + msg); - } - }; - - - /* DATEPICKER DATA-API - * ================== */ - - $(document).on( - 'focus.datepicker.data-api click.datepicker.data-api', - '[data-provide="datepicker"]', - function(e){ - var $this = $(this); - if ($this.data('datepicker')) - return; - e.preventDefault(); - // component click requires us to explicitly show it - datepickerPlugin.call($this, 'show'); - } - ); - $(function(){ - datepickerPlugin.call($('[data-provide="datepicker-inline"]')); - }); - -})); + d: date.getUTCDate(), // 日期(1-31) + D: dates[language].daysShort[date.getUTCDay()], // 星期缩写(如 'Mon') + DD: dates[language].days[date.getUTCDay()], // 星期全称(如 'Monday') + m: date.getUTCMonth() + 1, // 月份(1-12) + M: dates[language].monthsShort[date.getUTCMonth()], // 月份缩写(如 'Jan') + MM: dates[language].months[date.getUTCMonth()], // 月份全称(如 'January') + yy: date.getUTCFullYear().toString().substring(2), // 年份后两位(如 '24') + yyyy: date.getUTCFullYear() // 年份四位(如 '2024') + }; + val.dd = (val.d < 10 ? '0' : '') + val.d; // 补零(如 '05') + val.mm = (val.m < 10 ? '0' : '') + val.m; // 补零 + + // 拼接结果(格式部分+分隔符) + var result = [], seps = $.extend([], format.separators); + for (var i = 0; i <= format.parts.length; i++) { + if (seps.length) result.push(seps.shift()); // 添加分隔符 + result.push(val[format.parts[i]]); // 添加格式部分 + } + return result.join(''); + }, + + // 日历模板(表头、内容、表尾) + headTemplate: '«»', + contTemplate: '', + footTemplate: '', + + // 完整日历模板(包含所有视图) + template: '
'+ + '