144 lines
5.4 KiB
Vue
144 lines
5.4 KiB
Vue
<script setup>
|
|
import { computed, getCurrentInstance, onBeforeUnmount, ref } from "vue";
|
|
import { onPrehydrate } from "../composables/ssr";
|
|
import { useNuxtApp } from "../nuxt";
|
|
const props = defineProps({
|
|
locale: { type: String, required: false },
|
|
datetime: { type: [String, Number, Date], required: true },
|
|
localeMatcher: { type: String, required: false },
|
|
weekday: { type: String, required: false },
|
|
era: { type: String, required: false },
|
|
year: { type: String, required: false },
|
|
month: { type: String, required: false },
|
|
day: { type: String, required: false },
|
|
hour: { type: String, required: false },
|
|
minute: { type: String, required: false },
|
|
second: { type: String, required: false },
|
|
timeZoneName: { type: String, required: false },
|
|
formatMatcher: { type: String, required: false },
|
|
hour12: { type: Boolean, required: false, default: void 0 },
|
|
timeZone: { type: String, required: false },
|
|
calendar: { type: String, required: false },
|
|
dayPeriod: { type: String, required: false },
|
|
numberingSystem: { type: String, required: false },
|
|
dateStyle: { type: String, required: false },
|
|
timeStyle: { type: String, required: false },
|
|
hourCycle: { type: String, required: false },
|
|
relative: { type: Boolean, required: false },
|
|
title: { type: [Boolean, String], required: false }
|
|
});
|
|
const el = getCurrentInstance()?.vnode.el;
|
|
const renderedDate = el?.getAttribute("datetime");
|
|
const _locale = el?.getAttribute("data-locale");
|
|
const nuxtApp = useNuxtApp();
|
|
const date = computed(() => {
|
|
const date2 = props.datetime;
|
|
if (renderedDate && nuxtApp.isHydrating) {
|
|
return new Date(renderedDate);
|
|
}
|
|
if (!props.datetime) {
|
|
return /* @__PURE__ */ new Date();
|
|
}
|
|
return new Date(date2);
|
|
});
|
|
const now = ref(import.meta.client && nuxtApp.isHydrating && window._nuxtTimeNow ? new Date(window._nuxtTimeNow) : /* @__PURE__ */ new Date());
|
|
if (import.meta.client && props.relative) {
|
|
const handler = () => {
|
|
now.value = /* @__PURE__ */ new Date();
|
|
};
|
|
const interval = setInterval(handler, 1e3);
|
|
onBeforeUnmount(() => clearInterval(interval));
|
|
}
|
|
const formatter = computed(() => {
|
|
const { locale: propsLocale, relative, ...rest } = props;
|
|
if (relative) {
|
|
return new Intl.RelativeTimeFormat(_locale ?? propsLocale, rest);
|
|
}
|
|
return new Intl.DateTimeFormat(_locale ?? propsLocale, rest);
|
|
});
|
|
const formattedDate = computed(() => {
|
|
if (!props.relative) {
|
|
return formatter.value.format(date.value);
|
|
}
|
|
const diffInSeconds = (date.value.getTime() - now.value.getTime()) / 1e3;
|
|
const units = [
|
|
{ unit: "second", seconds: 1, threshold: 60 },
|
|
// 60 seconds → minute
|
|
{ unit: "minute", seconds: 60, threshold: 60 },
|
|
// 60 minutes → hour
|
|
{ unit: "hour", seconds: 3600, threshold: 24 },
|
|
// 24 hours → day
|
|
{ unit: "day", seconds: 86400, threshold: 30 },
|
|
// ~30 days → month
|
|
{ unit: "month", seconds: 2592e3, threshold: 12 },
|
|
// 12 months → year
|
|
{ unit: "year", seconds: 31536e3, threshold: Infinity }
|
|
];
|
|
const { unit, seconds } = units.find(({ seconds: seconds2, threshold }) => Math.abs(diffInSeconds / seconds2) < threshold) || units[units.length - 1];
|
|
const value = diffInSeconds / seconds;
|
|
return formatter.value.format(Math.round(value), unit);
|
|
});
|
|
const isoDate = computed(() => date.value.toISOString());
|
|
const title = computed(() => props.title === true ? isoDate.value : typeof props.title === "string" ? props.title : void 0);
|
|
const dataset = {};
|
|
if (import.meta.server) {
|
|
for (const prop in props) {
|
|
if (prop !== "datetime") {
|
|
const value = props?.[prop];
|
|
if (value) {
|
|
const propInKebabCase = prop.split(/(?=[A-Z])/).join("-");
|
|
dataset[`data-${propInKebabCase}`] = props?.[prop];
|
|
}
|
|
}
|
|
}
|
|
onPrehydrate((el2) => {
|
|
const now2 = window._nuxtTimeNow ||= Date.now();
|
|
const toCamelCase = (name, index) => {
|
|
if (index > 0) {
|
|
return name[0].toUpperCase() + name.slice(1);
|
|
}
|
|
return name;
|
|
};
|
|
const date2 = new Date(el2.getAttribute("datetime"));
|
|
const options = {};
|
|
for (const name of el2.getAttributeNames()) {
|
|
if (name.startsWith("data-")) {
|
|
const optionName = name.slice(5).split("-").map(toCamelCase).join("");
|
|
options[optionName] = el2.getAttribute(name);
|
|
}
|
|
}
|
|
if (options.relative) {
|
|
const diffInSeconds = (date2.getTime() - now2) / 1e3;
|
|
const units = [
|
|
{ unit: "second", seconds: 1, threshold: 60 },
|
|
// 60 seconds → minute
|
|
{ unit: "minute", seconds: 60, threshold: 60 },
|
|
// 60 minutes → hour
|
|
{ unit: "hour", seconds: 3600, threshold: 24 },
|
|
// 24 hours → day
|
|
{ unit: "day", seconds: 86400, threshold: 30 },
|
|
// ~30 days → month
|
|
{ unit: "month", seconds: 2592e3, threshold: 12 },
|
|
// 12 months → year
|
|
{ unit: "year", seconds: 31536e3, threshold: Infinity }
|
|
];
|
|
const { unit, seconds } = units.find(({ seconds: seconds2, threshold }) => Math.abs(diffInSeconds / seconds2) < threshold) || units[units.length - 1];
|
|
const value = diffInSeconds / seconds;
|
|
const formatter2 = new Intl.RelativeTimeFormat(options.locale, options);
|
|
el2.textContent = formatter2.format(Math.round(value), unit);
|
|
} else {
|
|
const formatter2 = new Intl.DateTimeFormat(options.locale, options);
|
|
el2.textContent = formatter2.format(date2);
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<time
|
|
v-bind="dataset"
|
|
:datetime="isoDate"
|
|
:title="title"
|
|
>{{ formattedDate }}</time>
|
|
</template>
|