strokoff

Расширяем возможности 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. Если вам понравился компонент и идея, поддержите звездочкой на гитхабе автора, спасибо)