import { createElement } from "../functions"; import { isPositive, nullOrEmpty } from "../utility"; /** * 创建或转换日期选择框 * @param {string} [min] - 最小可选日期 * @param {string} [max] - 最大可选日期 * @param {HTMLInputElement | string} [element] - 转换该元素为日期选择框 * @returns {HTMLInputElement} 返回创建或转换的日期选择框 */ export function createDateInput(min, max, element) { let date; if (typeof element === 'string') { element = document.querySelector(element); } if (element instanceof HTMLInputElement) { date = element; date.classList.add('ui-date-cell'); } else { date = createElement('input', 'ui-date-cell'); } date.required = true; date.type = 'date'; if (min != null) { date.min = min; } else { date.min = '1753-01-01'; } if (max != null) { date.max = max; } else { date.max = '9999-12-31'; } return date; } /** * 将日期转换为 `yyyy-MM-dd` 格式的字符串 * @param {Date} dt 要转换的日期值 * @param {boolean} [local] 是否视日期为本地时间 * @returns 返回 `yyyy-MM-dd` 格式的字符串 */ export function toDateValue(dt, local) { if (isNaN(dt)) { return ''; } const year = local ? dt.getFullYear() : dt.getUTCFullYear(); const month = String((local ? dt.getMonth() : dt.getUTCMonth()) + 1).padStart(2, '0'); const date = String(local ? dt.getDate() : dt.getUTCDate()).padStart(2, '0'); return `${year}-${month}-${date}`; } /** * 获取日期格式器 * @param {Date} date - 待格式的日期 * @param {boolean} [utc=true] - 是否按 UTC 时间格式化 * @returns {any} 返回格式化工具对象 */ export function getFormatter(date, utc) { const prefix = utc !== false ? 'getUTC' : 'get'; const r = { /** * @private * @returns {number} 返回日数字 */ j: () => date[`${prefix}Date`](), /** * @private * @returns {string} 返回格式化成两位的日字符串 */ d: () => String(r.j()).padStart(2, '0'), /** * @private * @returns {number} 返回月数字 */ n: () => date[`${prefix}Month`]() + 1, /** * @private * @returns {string} 返回格式化成两位的月字符串 */ m: () => String(r.n()).padStart(2, '0'), /** * @private * @returns {number} 返回年数字 */ Y: () => date[`${prefix}FullYear`](), /** * @private * @returns {string} 返回年后两位 */ y: () => String(r.Y()).slice(-2), /** * @private * @returns {number} 返回日期当月总天数 */ t: () => new Date(r.Y(), r.n(), 0).getDate(), /** * @private * @returns {('1' | '0')} 返回是否为闰年 */ L() { const y = r.Y(); return t % 4 === 0 && t % 100 !== 0 || t % 400 === 0 ? 1 : 0; }, /** * @private * @returns {number} 返回小时数字 */ G: () => date[`${prefix}Hours`](), /** * @private * @returns {number} 返回 12 小时制的数字 */ g: () => r.G() % 12 || 12, /** * @private * @returns {string} 返回格式化成两位的小时字符串 */ H: () => String(r.G()).padStart(2, '0'), /** * @private * @returns {string} 返回格式化成两位的 12 小时制的字符串 */ h: () => String(r.g()).padStart(2, '0'), /** * @private * @returns {string} 返回上下午 */ A: () => ['AM', 'PM'][r.G() < 12 ? 0 : 1], /** * @private * @returns {string} 返回小写的上下午 */ a: () => r.A().toLowerCase(), /** * @private * @returns {string} 返回格式化成两位的分钟字符串 */ i: () => String(date[`${prefix}Minutes`]()).padStart(2, '0'), /** * @private * @returns {string} 返回格式化成两位的秒字符串 */ s: () => String(date[`${prefix}Seconds`]()).padStart(2, '0'), /** * @private * @returns {string} 返回格式化成六位的毫秒字符串 */ u: () => String(1e3 * date[`${prefix}Milliseconds`]()).padStart(6, '0'), /** * @private * @returns {string} 返回时区描述字符串 */ e: () => /\((.*)\)/.exec(String(date))[1] || 'Coordinated Universal Time', /** * @private * @returns {string} 返回时区偏移字符串 */ O() { const t = date.getTimezoneOffset(); const r = Math.abs(t); return (t > 0 ? '-' : '+') + String(100 * Math.floor(r / 60) + r % 60).padStart(4, '0'); }, /** * @private * @returns {string} 返回 +08:00 这种格式的时区偏移字符串 */ P() { const t = r.O(); return t.substring(0, 3) + ':' + t.substring(3, 5); } }; return r; } /** * 格式化日期为 M/d/yyyy 格式的字符串 * @param {Date | number | string} date - 需要格式化的日期值,支持的格式如下: * * * `"2024-01-26"` * * `"2024/1/26"` * * `"2024-01-26T00:00:00"` * * `"1/26/2024"` * * `"638418240000000000"` * * `new Date('2024-01-26')` * @param {string} [formatter] - 格式化格式,默认为 `"m/d/Y"` * * * Y - 年,例如 `2024` * * y - 年的后两位,例如 `"24"` * * n - 月,例如 `2` * * m - 格式化成两位的月,例如 `"02"` * * j - 日,例如 `4` * * d - 格式化成两位的日,例如 `"04"` * * t - 日期当月总天数,例如 `29` * * L - 是否为闰年,例如 `1` * * G - 小时,例如 `15` * * g - 12 小时制的小时,例如 `3` * * H - 格式化成两位的小时,例如 `"15"` * * h - 格式化成两位的 12 小时制的小时,例如 `"03"` * * A - 上下午,例如 `"PM"` * * a - 小写的上下午,例如 `"pm"` * * i - 格式化成两位的分钟,例如 `"39"` * * s - 格式化成两位的秒,例如 `"20"` * * u - 格式化成六位的毫秒,例如 `"023040"` * * e - 时区描述字符串,例如 `"China Standard Time"` * * O - 时区偏移字符串,例如 `"+0800"` * * P - `"+08:00"` 这种格式的时区偏移字符串 * @returns {string} 返回格式化后的日期字符串 */ export function formatDate(date, formatter) { formatter ??= 'm/d/Y'; if (date === '') { return ''; } if (isNaN(date)) { let e = /^(\d{4})-(\d{2})-(\d{2})/.exec(date); if (e == null) { e = /^(\d{4})\/(\d{1,2})\/(\d{1,2})/.exec(date); } if (e != null) { date = new Date(e[1], parseInt(e[2]) - 1, e[3]); } else { e = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(date); if (e != null) { date = new Date(e[3], parseInt(e[1]) - 1, e[2]); } else { return date; } } } if (date instanceof Date) { const f = getFormatter(date, false); return formatter.replace(/\\?(.?)/gi, (k, v) => f[k] ? f[k]() : v); } const ticks = Number(date); if (!isNaN(ticks) && ticks > 0) { date = new Date((ticks - 621355968e9) / 1e4); const f = getFormatter(date); return formatter.replace(/\\?(.?)/gi, (k, v) => f[k] ? f[k]() : v); } return date; } /** * 设置显示日期 * @param {HTMLElement} element - 要设置显示日期的元素 * @param {Date | number | string} val - 日期值,支持格式参见 {@linkcode formatDate} * @param {string} [formatter] - 日期格式化字符串(仅设置显示元素时调用) */ export function setDateValue(element, val, formatter) { if (element.tagName === 'INPUT') { if (val === '') { element.value = ''; } else if (isNaN(val)) { if (/^\d{4}-\d{2}-\d{2}/.test(val)) { element.value = String(val).substring(0, 10); } else if (/^\d{4}\/\d{2}\/\d{2}/.test(val)) { element.value = String(val).replace('/', '-').substring(0, 10); } else { const e = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(val); if (e != null) { const month = e[1].padStart(2, '0'); const day = e[2].padStart(2, '0'); const year = e[3]; element.value = `${year}-${month}-${day}`; } else { element.value = ''; } } } else { if (val instanceof Date) { element.value = toDateValue(val, true); } else { const ticks = Number(val); if (!isNaN(ticks) && ticks > 0) { element.value = toDateValue(new Date((ticks - 621355968e9) / 1e4)); } else { element.value = ''; } } } } else { element.innerText = formatDate(val, formatter); } } /** * 自定义日期格式化回调函数 * @callback DateFormatterCallback * @param {Date} date - 日期值 * @returns {any} 返回格式化后的结果 */ /** * 从日期选择框获取日期值 * @param {HTMLInputElement} element - 要获取的日期选择框 * @param {string | DateFormatterCallback} [formatter] - 自定义格式化字符串或函数,传入参数为 `Date` 类型 * @returns {string | any} 默认返回日期 `ticks` 的字符串 */ export function getDateValue(element, formatter) { const date = element?.valueAsDate; if (date instanceof Date && !isNaN(date)) { const year = date.getUTCFullYear(); if (year < 1900 || year > 9999) { return ''; } if (formatter != null) { const month = String(date.getUTCMonth() + 1).padStart(2, '0'); const day = String(date.getUTCDate()).padStart(2, '0'); // 使外部 formatter 不需要再处理 `getUTCDate` 亦或是 `getDate` const localDate = new Date(`${year}-${month}-${day}T00:00:00`); if (typeof formatter === 'function') { return formatter(localDate); } return formatDate(localDate, formatter); } return String(date.getTime() * 1e4 + 621355968e9); } return ''; } /** * 日期选择框类 * @class */ export class DateSelector { _var = { /** * @type {HTMLInputElement} * @private */ el: null, options: { /** * @type {boolean?} * @private */ enabled: true, /** * @type {string?} * @private */ minDate: null, /** * @type {string?} * @private */ maxDate: null, /** * @type {number?} * @private */ tabIndex: null, /** * @type {string?} * @private */ className: null, /** * @type {DateFormatterCallback?} * @private */ valueFormatter: null } }; /** * 日期发生变化时触发的事件 * @event * @param {Date} date - 修改后的日期值 * @this DateSelector */ onDateChanged; /** * 日期选择框构造函数 * @constructor * @param {object} [opts] - 日期选择框初始化参数 * @param {boolean} [opts.enabled] - 是否可用 * @param {string} [opts.minDate] - 最小可选择日期 * @param {string} [opts.maxDate] - 最大可选择日期 * @param {DateFormatterCallback} [opts.valueFormatter] - 自定义格式化函数 * @example <caption>创建一个 DateSelector 并插入到元素中</caption> * <div id="dateFrom"></div> * @example * // 构造参数 * const opts = { * minDate: '2020-01-01', * maxDate: '2024-02-01', * valueFormatter: (date) => { * return `${date.getUTCFullYear()}/${date.getUTCMonth() + 1}/${date.getUTCDate()}`; // 2024/1/31 * } * }; * const dateSelectorFrom = new DateSelector(opts); * document.querySelector('#dateFrom').appendChild(dateSelectorFrom.create()); * @example <caption>将一个元素转化为 DateSelector</caption> * <input id="dateTo"></div> * @example * const dateSelectorTo = new DateSelector(); * dateSelectorTo.create('#dateTo'); */ constructor(opts) { this._var.options = opts ?? {}; } /** * 创建或转换日期选择框元素 * @param {HTMLInputElement | string} [element] - 转换该元素为日期选择框 * @returns {HTMLInputElement} 返回创建或转换的日期选择元素 */ create(element) { const opts = this._var.options; const el = createDateInput(opts.minDate, opts.maxDate, element); isPositive(opts.tabIndex) && el.setAttribute('tabindex', opts.tabIndex); if (!nullOrEmpty(opts.className)) { el.classList.add(opts.className); } if (element == null) { el.disabled = opts.enabled === false; } el.addEventListener('blur', e => { const date = this._getDate(e.target.valueAsDate); if (date == null) { e.target.value = ''; } if (typeof this.onDateChanged === 'function') { this.onDateChanged(date); } }); this._var.el = el; return el; } /** * 获取日期选择框元素 * @readonly * @type {HTMLInputElement} */ get element() { return this._var.el } /** * 获取或设置日期选择框是否启用 * @type {boolean} */ get enabled() { return !this._var.el.disabled } set enabled(flag) { this._var.el.disabled = flag === false; } /** * 获取格式化的日期值,或设置日期值,支持的格式参见 {@linkcode formatDate} * @type {string | any} */ get value() { return this._getDate(this._var.el.valueAsDate) } set value(val) { setDateValue(this._var.el, val); } /** * 获取或设置最小可选择日期 * @type {string} */ get minDate() { return this._var.el.min } set minDate(date) { this._var.el.min = date; this._var.options.minDate = date; } /** * 获取或设置最大可选择日期 * @type {string} */ get maxDate() { return this._var.el.max } set maxDate(date) { this._var.el.max = date; this._var.options.maxDate = date; } /** * @private * @param {Date} date * @returns {Date | any} */ _getDate(date) { if (date instanceof Date && !isNaN(date)) { const year = date.getUTCFullYear(); if (year < 1900 || year > 9999) { return null; } if (typeof this._var.options.valueFormatter === 'function') { return this._var.options.valueFormatter(date); } return date; } return null; } /** * 把父容器下所有匹配 `input[data-type="date"]` 的元素修改为统一的日期选择框<br/><br/> * 解析的属性为 `id`, `class`, `data-min`, `data-max`, `disabled` * @static * @param {HTMLElement} [dom=document.body] 父元素 * @param {Function} [trigger] 日期设置事件触发函数 * @param {Date} trigger.{this} - 上下文为触发设置日期的 `DateSelector` 实例 * @param {Date} trigger.date - 修改后的日期值 * @example <caption>HTML</caption> * <input id="dateFrom" data-type="date" data-min="1980-01-01"/> * @example <caption>解析父容器</caption> * // 解析 document.body 下所有符合条件的元素,转换为日期选择框,第二个参数可选 * DateSelector.resolve(document.body, function (date) { * console.log(`element(#${this.element.id}), date changed to: ${formatDate(date)}`); * // 当日期选择改变时,控制台将会输出:element(#dateFrom), date changed to: 1/30/2024 * }); * @example <caption>其他地方调用时</caption> * const value = document.querySelector('#dateFrom').value; * console.log(`dateFrom.value = '${value}', formatted: '${formatDate(value)}'`); * // 控制台会输出:dateFrom.value = '2024-01-30', formatted: '1/30/2024' */ static resolve(dom = document.body, trigger) { const dates = dom.querySelectorAll('input[data-type="date"]'); for (let dat of dates) { const val = dat.value; const dateSelector = new DateSelector({ minDate: dat.getAttribute('data-min'), maxDate: dat.getAttribute('data-max') }); if (typeof trigger === 'function') { dateSelector.onDateChanged = date => trigger.call(dateSelector, date); } dat.removeAttribute('data-type'); dat.removeAttribute('data-min'); dat.removeAttribute('data-max'); dateSelector.create(dat); dateSelector.value = val; } } }