Расширяем возможности HTML5 тега time с помощью веб-компонента wc-time
Текушее время и мы продолжаем наш цикл статей про разработку веб-компонентов и сегодня мы будем расширять возможности HTML5 тега time с помощью возможностей браузера и идеологии веб-компонентов
<time is="wc-time"></time>
Мультиязычность с помощью lang аттрибута
По умолчанию будем ориентироваться на navigator.language или на указанный в теге аттрибут lang, пропишем эту логику в конструкторе веб-компонента wc-likes
this.Lang = this.getAttribute('lang') || navigator.language.replace(/-\w{1,2}/gm, "");
Проверим, как получается
1 febrary lang=ru 🇷🇺
13 мая lang=en 🏴
<time is="wc-time" lang="ru" datetime="2023-02-01">1 febrary 2023</time>
Добавим кейс, что текущий год wc-time скрывает по умолчанию т.к. дата актуальна в контексте текущего года. Посмотрим на дату из другого года
1 февраля 2020 lang=en 🏴
Для удобочитаемости и поддержки склонения разных языков, добавим вспомогательную функцию склонения слов и фразы для двух самых популярных языков в интернете
Функция склонения и набор фраз
/**
* Declension
* @param value
* @param words
* @returns
*/
const nw = (value:number, words:string[]):string => {
value = Math.abs(value) % 100;
var num = value % 10;
if(value > 10 && value < 20) return words[2];
if(num > 1 && num < 5) return words[1];
if(num == 1) return words[0];
return words[2];
}
/**
* Default translates for TOP2 internet languages
*/
const Translate = {
en: {
ago: 'ago',
year: ['year', 'years', 'years'],
month: ['month', 'months', 'months'],
week: ['week', 'weeks', 'weeks'],
day: ['day', 'days', 'days'],
hour: ['hour', 'hours', 'hours'],
minute: ['minute', 'minutes', 'minutes'],
},
ru: {
ago: 'назад',
year: ['год', 'года', 'лет'],
month: ['месяц', 'месяца', 'месяцев'],
week: ['неделя', 'недели', 'недель'],
day: ['день', 'дня', 'дней'],
hour: ['час', 'часа', 'часов'],
minute: ['минута', 'минуты', 'минут'],
}
};
Формат вывода даты\времени\прошедшего времени data-view-format
При помощи data-view-format аттрибута мы можем управлять видом выводимого времени в time. По умолчанию, установим, что будет показываться дата и время (datetime) date,time
Проверим комбинации
<time is="wc-time" data-view-format="date" datetime="2022-02-01"></time>
data-view-format="time"
data-view-format="date"
data-view-format="ago"
data-view-format="time,ago"
Формат вывода даты в всплывающем окне data-dialog-format
Добавим поддержку настройки формата вывода даты в всплывающем dialog при помощи data-dialog-format аттрибута, установим значение по умолчанию максимально возможное — date,time,ago
data-dialog-format="time"
data-dialog-format="date"
data-dialog-format="ago"
data-dialog-format="time,ago"
По умолчанию диалоговое окно показывается, добавим возможность указать data-show-dialog="0" чтобы не загружать HTML разметку. Кстати про нагрузку разметки, давай будем показывать в DOM dialog элемент только при событиях pointerenter и скрываем при pointercancel pointerleave
//in wc-time constructor //Listen pointer events in
Методы показа и закрытия диалогового окна очень просты
hideDateDialog() {
if(this.ShowDialog && this.DateDialog instanceof HTMLDialogElement) {
this.DateDialog.close();
this.DateDialog.parentElement.removeChild(this.DateDialog);
this.DateDialog = null;
}
}
showDateDialog() {
if(this.ShowDialog && this.DateCssHint === false) {
this.DateDialog = document.createElement('dialog');
this.DateDialog.style.whiteSpace = 'nowrap';
this.append(this.DateDialog);
this.DateDialog.innerHTML = this.renderTimeString(this.DateDialogFormat);
if(this.DateHint) {
const spanHint = document.createElement('span');
spanHint.classList.add('-hint');
spanHint.innerText = this.DateHint;
this.DateDialog.prepend(spanHint);
}
this.DateDialog.show();
}
}
Проверим что получается
<time is="wc-time" data-show-dialog="0" data-view-format="date" datetime="2022-02-01"></time>
Переиспользуем Intl.DateTimeFormat возможности через аттрибуты
Более подробно про возможности Intl.DateTimeFormat() читайте тут
// prepare Intl.DateTimeFormat options
const language = this.Lang;
const defaultIntDateOptions = this.defaultIntDateOptions = {
timeZone: this.getAttribute('data-format-timeZone') || Intl.DateTimeFormat().resolvedOptions().timeZone,
hour12: this.getAttribute('data-format-hour12') !== null || false,
weekday: this.getAttribute('data-format-weekday') as "narrow" | "short" | "long" || 'long',
era: this.getAttribute('data-format-era') as "narrow" | "short" | "long" || 'long',
year: this.getAttribute('data-format-year') as "numeric" | "2-digit" || 'numeric',
month: this.getAttribute('data-format-month') as "narrow" | "short" | "long" | "numeric" | "2-digit" || 'long',
day: this.getAttribute('data-format-day') as "numeric" | "2-digit" || 'numeric',
hour: this.getAttribute('data-format-hour') as "numeric" | "2-digit" || '2-digit',
minute: this.getAttribute('data-format-minute') as "numeric" | "2-digit" || '2-digit',
second: this.getAttribute('data-format-second') as "numeric" | "2-digit" || '2-digit',
timeZoneName: this.getAttribute('data-format-timeZoneName') as "short" | "long" | "shortOffset" | "longOffset" | "shortGeneric" | "longGeneric" || 'short',
};
//short usage intl in component context
this.intl =(propDateNames:string[], declension = false) => {
let options = {};
for (let i = 0; i < propDateNames.length; i++) {
const propDateName = propDateNames[i];
options[propDateName] = defaultIntDateOptions[propDateName];
if(declension) {
if(propDateName === 'month') {
options['day'] = defaultIntDateOptions['day'];
return new Intl.DateTimeFormat([language, 'en'], options).format(this.Date).replace(/\d\d? /, '');
}
}
if(propDateName === 'era') {
options['year'] = 'numeric';
}
}
return new Intl.DateTimeFormat([language, 'en'], options).format(this.Date);
}
Проверяем, что получилось
data-format-month="numeric"
data-format-year="2-digit"
data-format-date="era,day,month,weekday"
<time is="wc-time" data-format-date="era,day,month,weekday" datetime="2012-05-22"></time>
data-format-time="minute,hour,second"
<time is="wc-time" data-view-format="time" data-format-date="minute,hour,second" datetime="2012-05-22"></time>
Наблюдаемые изменения за аттрибутом
Из коробки веб-компоненты дают нам возможность наблюдать за изменениями аттриубтов, грех не воспользоваться такой возможностью, добавим поведение на изменение нативного аттрибута datetime у тега time. Теперь можем посмотреть, как быстро летит время)
<time is="wc-time" id="timereact2" data-view-format="date,time,ago" data-format-date="era,day,month,weekday" data-format-time="minute,hour,second" data-view-format="date" datetime="2012-05-22"></time>
Пример простенького javascript изменяющего аттрибут, тут мог бы быть и ваш любимый frontend js фреймворк)
document.addEventListener('DOMContentLoaded', e => {
let now = Date.now();
const timereact = document.getElementById('timereact');
const timereact2 = document.getElementById('timereact2');
setInterval(() => {
now += 100001231;
timereact.setAttribute('datetime', new Date(Date.now()).toISOString());
timereact2.setAttribute('datetime', new Date(now).toISOString());
}, 100)
})
Рендерим wc-time в текстовом режиме
По умолчанию wc-time использует span для оборачивания частей текста в теги, это позволяет кастомизировать веб-компонент при помощи CSS но давайте добавим возможность указать data-as-string="1" чтобы внутри тега рендерилась только строка
а что если к месту и элемент dialog не удобен, но подсказки хочется оставить без лишних dom элементов, добавим поддержку и такого варианта через data-css-hint="1"
Проверим, наведите курсор ->
Но пользователь все еще не может добавить свой текст как подсказку к дате в нашем всплывающем блоке, добавим и этот момент при помощи аттрибута data-date-hint="foo bar biz" который мы будем prepend к нашей dialog\css подсказке
wc-likes ссылки
p.s. Если вам понравился компонент и идея, поддержите звездочкой на гитхабе автора, спасибо)