521 lines
17 KiB
JavaScript
521 lines
17 KiB
JavaScript
import { createElement } from "../functions";
|
||
|
||
/**
|
||
* 创建或转换日期选择框
|
||
* @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}`;
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {Date} date
|
||
* @param {boolean} [utc=true]
|
||
* @returns {string}
|
||
*/
|
||
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-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{2})\/(\d{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 {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);
|
||
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;
|
||
}
|
||
}
|
||
} |