ui-lib/lib/ui/date.js
2024-08-30 17:36:21 +08:00

537 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}