UNPKG

timered-counter

Version:

Make the value change more vivid and natural

222 lines 9.7 kB
import { __decorate } from "tslib"; import { customElement, property } from 'lit/decorators.js'; import { html } from 'lit'; import { repeat } from 'lit/directives/repeat.js'; import { isArray, isDate, isNullish, isString, map } from 'remeda'; import { isValid, toDate, isBefore } from 'date-fns'; import { TimeredCounter } from './timered-counter.js'; import { DurationPartMillisecond, DurationPartType } from './types/duration.js'; import { getLocalizedDateTimeFields } from './utils/localized-date-time-fields.js'; import { duration, durationObject } from './utils/duration.js'; import { iso8601Duration } from './utils/iso8601-duration.js'; import { timeredCounterDatetimeStyles } from './styles/timered-counter-datetime-styles.js'; import { parseJsonString } from './utils/parse-json-string.js'; /** * 根据最小精度对 from 进行优化. 避免频繁更新. * * 1. from 先会被减小到 minPrecisionMs 的整数倍. * 2. 如果 from 和 to 的差值不是 minPrecisionMs 的整数倍, 则再加上/减去一个 minPrecisionMs 的值. 加上或减去取决于 from 和 to 的谁更大. * 这相当于将 from 中小于 minPrecisionMs 的值舍入到 minPrecisionMs 的整数倍. * 3. 加上 to 余 minPrecisionMs 的值, 保证 from 与 to 的差值是 minPrecisionMs 的整数倍. */ function optimizeFrom(from, to, minPrecision) { const minPrecisionMs = DurationPartMillisecond[minPrecision]; const fromTS = from.getTime(); const toTS = to.getTime(); const base = fromTS - (fromTS % minPrecisionMs); const offset = Math.abs(toTS - fromTS) % minPrecisionMs; return (base + (offset > 0 ? (fromTS < toTS ? -1 : 1) * minPrecisionMs : 0) + // 加上 deadlineDate 的余数, 消除精度误差. (toTS % minPrecisionMs)); } function toDurationInMilliseconds(value, minPrecision) { if (isString(value)) value = parseJsonString(value); if (!isArray(value)) value = [value, value]; const result = [ isDate(value[0]) ? value[0] : toDate(value[0]), isDate(value[1]) ? value[1] : toDate(value[1]), ]; if (!isValid(result[0]) || !isValid(result[1])) { throw new Error(`value ${value[0]} or ${value[1]} is not a valid date.`); } const durationInMilliseconds = Math.abs(result[1].getTime() - optimizeFrom(result[0], result[1], minPrecision)); return { durationInMilliseconds, from: result[0], to: result[1], }; } let TimeredCounterDatetimeDuration = class TimeredCounterDatetimeDuration extends TimeredCounter { constructor() { super(...arguments); this.__precision = [DurationPartType.Second, DurationPartType.Day]; this.__initialValuePlain = null; this.__partsOptions = null; this.__from = new Date(); this.__to = new Date(); this.__minPrecision = DurationPartType.Second; this.__maxPrecision = DurationPartType.Day; this.__availableDurationParts = []; this.__dateTimeFieldLabels = {}; } /** * 计数器显示的精度. * 1. 当为单个值时, 表示最小精度. * 2. 当为数组时, 第一个值表示最小精度, 第二个值表示最大精度. * * @default [DurationPartType.Second, DurationPartType.Day] * * @example DurationPartType.Second 显示从年份到秒数的所有精度. * @example [DurationPartType.Second, DurationPartType.Day] 显示从天数到秒数的所有精度. * @example [DurationPartType.Millisecond, DurationPartType.Year] 显示从年份到毫秒的所有精度. */ get precision() { return this.__precision; } set precision(value) { if (isString(value)) value = parseJsonString(value); this.__precision = value; /** * `precision` 相关属性的更新需要立即更新, 以便于在 `value`, `initialValue` 等属性的 setter 中使用. */ this.__minPrecision = isArray(this.__precision) ? this.__precision[0] : this.__precision; this.__maxPrecision = isArray(this.__precision) ? this.__precision[1] : this.__precision; this.__availableDurationParts = Object.values(DurationPartType) .reverse() .map(type => { const minPrecisionBreakpoint = DurationPartMillisecond[this.__minPrecision]; const maxPrecisionBreakpoint = DurationPartMillisecond[this.__maxPrecision]; const partMilliseconds = DurationPartMillisecond[type]; return { type, available: partMilliseconds >= minPrecisionBreakpoint && partMilliseconds <= maxPrecisionBreakpoint, }; }) .filter(part => part.available); } get value() { return super.value; } /** * 通过 property 设置 value 时, 支持 Date 类型. */ set value(value) { const { from, to, durationInMilliseconds } = toDurationInMilliseconds(value, this.__minPrecision); this.__from = from; this.__to = to; super.value = durationInMilliseconds; } get initialValue() { return super.initialValue; } /** * 同 value */ set initialValue(value) { this.__initialValuePlain = value; const { durationInMilliseconds } = toDurationInMilliseconds(value, this.__minPrecision); super.initialValue = durationInMilliseconds; } get partsOptions() { return super.partsOptions; } set partsOptions(value) { this.__partsOptions = value; super.partsOptions = { minPlaces: [2, undefined], ...this.__partsOptions, }; } sampleSplit(samples) { const availableDurationPartTypes = map(this.__availableDurationParts, part => part.type); const tempParts = availableDurationPartTypes.map(() => []); for (const n of samples) { const num = this.numberAdapter.toNumber(n); /** * 计算并保存每个在 {@link precision} 范围内的时间部分的值 */ const availablePartValues = duration(new Date(Math.min(num, 0)), new Date(Math.max(num, 0)), availableDurationPartTypes); availablePartValues.forEach((value, i) => tempParts[i].push(value)); } return tempParts; } generateAriaLabel() { return iso8601Duration(durationObject(isBefore(this.__from, this.__to) ? this.__from : this.__to, isBefore(this.__from, this.__to) ? this.__to : this.__from, map(this.__availableDurationParts, part => part.type))); } connectedCallback() { this.role = 'timer'; /** * TimeredCounterDatetimeDuration 将 `minPlaces` 默认设置为 `[2]`. 实例化时需要手动触发 `partsOptions` 的 setter. */ this.partsOptions = this.__partsOptions ?? {}; this.initialValue = this.__initialValuePlain; /** * 类似上方的 partsOptions, precision 也需要在初始化时手动触发 setter. 以此确保 __minPrecision, __maxPrecision, __availableDurationParts 有值. */ this.precision = this.__precision; super.connectedCallback(); } willUpdate(_changedProperties) { super.willUpdate(_changedProperties); if (_changedProperties.has('locale')) { this.__dateTimeFieldLabels = getLocalizedDateTimeFields(this.localeInstance); } } render() { const cellStyles = this.extractCellStyles(); const digitStyles = this.extractDigitStyles(); const partStyles = this.extractPartStyles(); const animationOptions = this.extractAnimationOptions(); const keyframes = this.extractKeyframes(); const availableDurationPartTypes = map(this.__availableDurationParts, part => part.type); return html ` <timered-counter-roller class="timered-counter timered-counter-datetime-duration" exportparts="group, part, digit, cell, prefix, suffix, part-suffix" part="group" aria-hidden="true" color=${this.color} .parentContainerRect=${this.partsContainerRect} .parts=${this.parts} .partPreprocessDataList=${this.partPreprocessDataList} .animationOptions=${animationOptions} .keyframes=${keyframes} .cellStyles=${cellStyles} .digitStyles=${digitStyles} .partStyles=${partStyles} .direction=${this.direction} @roller-animation-start=${this.dispatchTimeredCounterAnimationStart} @roller-animation-end=${this.dispatchTimeredCounterAnimationEnd} ><slot name="prefix" slot="prefix"></slot ><slot name="suffix" slot="suffix"></slot>${repeat(this.parts, (_, index) => index, (_, partIndex) => html `<span slot=${`part-suffix-${partIndex}`} class="duration-unit" >${this.__dateTimeFieldLabels[availableDurationPartTypes[partIndex]]}</span >`)} </timered-counter-roller> `; } }; TimeredCounterDatetimeDuration.styles = [...TimeredCounter.styles, timeredCounterDatetimeStyles]; __decorate([ property({ reflect: true, converter: value => { if (isNullish(value)) return value; return parseJsonString(value); }, }) ], TimeredCounterDatetimeDuration.prototype, "precision", null); TimeredCounterDatetimeDuration = __decorate([ customElement('timered-counter-datetime-duration') ], TimeredCounterDatetimeDuration); export { TimeredCounterDatetimeDuration }; //# sourceMappingURL=timered-counter-datetime-duration.js.map