Создание календарей с учетом доступности и интернационализации

Создание календарей с учетом доступности и интернационализации

Исходный узел: 2009236

Быстрый поиск здесь, в CSS-Tricks, показывает, сколько существует разных способов работы с календарями. Некоторые показывают, как CSS Grid может эффективно создавать макет. Некоторые попытки привнести фактические данные в микс. Некоторые полагаться на структуру помогать в управлении государством.

При создании компонента календаря нужно учитывать множество соображений — гораздо больше, чем описано в статьях, на которые я ссылался. Если подумать, календари полны нюансов, от обработки часовых поясов и форматов даты до локализации и даже обеспечения того, чтобы даты перетекали из одного месяца в другой… отображается и не только.

Многие разработчики опасаются Date() объект и придерживайтесь старых библиотек, таких как moment.js. Но несмотря на то, что когда дело доходит до дат и форматирования, есть много «подводных камней», в JavaScript есть много крутых API и других вещей, которые могут помочь!

Календарная сетка на январь 2023 года.

Я не хочу воссоздавать колесо здесь, но я покажу вам, как мы можем получить чертовски хороший календарь с помощью ванильного JavaScript. мы рассмотрим доступность, используя семантическую разметку и удобный для чтения с экрана <time> -tags — а также интернационализация и форматирование, С использованием Intl.Locale, Intl.DateTimeFormat и Intl.NumberFormat-API.

Другими словами, мы делаем календарь… только без дополнительных зависимостей, которые вы обычно видите в подобном руководстве, и с некоторыми нюансами, которые вы обычно не замечаете. И в процессе, я надеюсь, вы по-новому оцените новые возможности JavaScript, а также получите представление о тех вещах, которые приходят мне в голову, когда я собираю что-то подобное.

Во-первых, именование

Как мы должны назвать наш компонент календаря? На моем родном языке это называлось бы «элемент календаря», так что давайте воспользуемся этим и сократим до «Кал-Эл», также известного как Имя Супермена на планете Криптон.

Давайте создадим функцию, чтобы все заработало:

function kalEl(settings = {}) { ... }

Этот метод будет отображать один месяц. Позже мы будем вызывать этот метод из [...Array(12).keys()] рендерить целый год.

Исходные данные и интернационализация

Одна из распространенных функций типичного онлайн-календаря — выделение текущей даты. Итак, давайте создадим ссылку для этого:

const today = new Date();

Далее мы создадим «объект конфигурации», который мы объединим с необязательным settings объект основного метода:

const config = Object.assign( { locale: (document.documentElement.getAttribute('lang') || 'en-US'), today: { day: today.getDate(), month: today.getMonth(), year: today.getFullYear() } }, settings
);

Проверяем, если корневой элемент (<html>) содержит lang-атрибут с местный Информация; в противном случае мы вернемся к использованию en-US. Это первый шаг к интернационализация календаря.

Нам также необходимо определить, какой месяц будет отображаться при рендеринге календаря. Именно поэтому мы расширили config объект с первичным date. Таким образом, если дата не указана в settings объект, мы будем использовать today ссылка вместо этого:

const date = config.date ? new Date(config.date) : today;

Нам нужно немного больше информации, чтобы правильно отформатировать календарь в зависимости от локали. Например, мы можем не знать, является ли первый день недели воскресеньем или понедельником, в зависимости от региона. Если у нас есть информация, отлично! Но если нет, мы обновим его с помощью Intl.Locale API. API имеет weekInfo объект который возвращает firstDay свойство, которое дает нам именно то, что мы ищем без каких-либо хлопот. Мы также можем получить, какие дни недели назначены для weekend:

if (!config.info) config.info = new Intl.Locale(config.locale).weekInfo || { firstDay: 7, weekend: [6, 7] };

Опять же, мы создаем запасные варианты. «Первый день» недели для en-US воскресенье, поэтому по умолчанию используется значение 7. Это немного сбивает с толку, так как getDay метод в JavaScript возвращает дни как [0-6], Где 0 воскресенье… не спрашивайте меня, почему. Выходные - суббота и воскресенье, поэтому [6, 7].

Раньше у нас был Intl.Locale API и его weekInfo метод, было довольно сложно создать международный календарь без множества **объектов и массивов с информацией о каждой локали или регионе. В наше время это легко. Если мы пройдем в en-GB, метод возвращает:

// en-GB
{ firstDay: 1, weekend: [6, 7], minimalDays: 4
}

В такой стране, как Бруней (ms-BN), выходные - пятница и воскресенье:

// ms-BN
{ firstDay: 7, weekend: [5, 7], minimalDays: 1
}

Вы можете задаться вопросом, что это minimalDays свойство есть. Это наименьшее количество дней, необходимых в первую неделю месяца, чтобы считаться полной неделей. В некоторых регионах это может быть всего один день. Для других это могут быть полные семь дней.

Далее мы создадим render метод в рамках нашего kalEl-метод:

const render = (date, locale) => { ... }

Нам все еще нужны некоторые данные для работы, прежде чем мы что-то отрендерим:

const month = date.getMonth();
const year = date.getFullYear();
const numOfDays = new Date(year, month + 1, 0).getDate();
const renderToday = (year === config.today.year) && (month === config.today.month);

Последний - это Boolean это проверяет, есть ли today существует в том месяце, который мы собираемся визуализировать.

Семантическая разметка

Через мгновение мы углубимся в рендеринг. Но сначала я хочу убедиться, что детали, которые мы настраиваем, имеют семантические теги HTML, связанные с ними. Установка этого прямо из коробки дает нам преимущества доступности с самого начала.

Оболочка календаря

Во-первых, у нас есть несемантическая оболочка: <kal-el>. Это нормально, потому что нет семантического <calendar> тег или что-то в этом роде. Если бы мы не делали пользовательский элемент, <article> может быть наиболее подходящим элементом, поскольку календарь может располагаться на отдельной странице.

Названия месяцев

Ассоциация <time> Этот элемент будет для нас важным, потому что он помогает переводить даты в формат, который программы чтения с экрана и поисковые системы могут анализировать более точно и последовательно. Например, вот как мы можем передать «январь 2023» в нашей разметке:

<time datetime="2023-01">January <i>2023</i></time>

Названия дней

Строка над датами календаря, содержащая названия дней недели, может быть сложной. Было бы идеально, если бы мы могли написать полные названия для каждого дня — например, воскресенья, понедельника, вторника и т. д. — но это может занять много места. Итак, давайте пока сократим имена внутри <ol> где каждый день <li>:

<ol> <li><abbr title="Sunday">Sun</abbr></li> <li><abbr title="Monday">Mon</abbr></li> <!-- etc. -->
</ol>

Мы могли бы поэкспериментировать с CSS, чтобы получить лучшее из обоих миров. Например, если мы изменили разметку примерно так:

<ol> <li> <abbr title="S">Sunday</abbr> </li>
</ol>

… мы получаем полные имена по умолчанию. Затем мы можем «скрыть» полное имя, когда закончится место, и отобразить title атрибут вместо этого:

@media all and (max-width: 800px) { li abbr::after { content: attr(title); }
}

Но мы не идем по этому пути, потому что Intl.DateTimeFormat Здесь также может помочь API. Мы вернемся к этому в следующем разделе, когда будем рассматривать рендеринг.

Номера дней

Каждой дате в календарной сетке присваивается номер. Каждое число является элементом списка (<li>) в упорядоченном списке (<ol>), и встроенный <time> Тег оборачивает фактический номер.

<li> <time datetime="2023-01-01">1</time>
</li>

И хотя я пока не планирую делать какие-либо стили, я знаю, что мне понадобится какой-то способ стилизовать числа дат. Это возможно как есть, но я также хочу иметь возможность оформлять номера рабочих дней иначе, чем номера выходных, если мне нужно. Итак, я собираюсь включить data-* Атрибуты специально для этого: data-weekend и data-today.

Номера недель

В году 52 недели, иногда 53. Хотя это не очень распространено, может быть неплохо отображать число для данной недели в календаре для дополнительного контекста. Мне нравится иметь его сейчас, даже если я не перестану им пользоваться. Но мы полностью используем его в этом уроке.

Мы будем использовать data-weeknumber в качестве крючка стиля и включите его в разметку для каждой даты, которая является первой датой недели.

<li data-day="7" data-weeknumber="1" data-weekend=""> <time datetime="2023-01-08">8</time>
</li>

Рендеринг

Давайте разместим календарь на странице! Мы уже знаем, что <kal-el> это имя нашего пользовательского элемента. Первое, что нам нужно настроить, это установить firstDay свойство на нем, поэтому календарь знает, является ли воскресенье или какой-либо другой день первым днем ​​недели.

<kal-el data-firstday="${ config.info.firstDay }">

Мы будем использовать литералы шаблона для рендеринга разметки. Чтобы отформатировать даты для международной аудитории, мы будем использовать Intl.DateTimeFormat API, снова используя locale мы указали ранее.

Месяц и год

Когда мы называем month, мы можем установить, хотим ли мы использовать long имя (например, февраль) или short имя (например, февраль). Давайте использовать long имя, так как это заголовок над календарем:

<time datetime="${year}-${(pad(month))}"> ${new Intl.DateTimeFormat( locale, { month:'long'}).format(date)} <i>${year}</i>
</time>

Названия дней недели

Для дней недели, отображаемых над сеткой дат, нам нужны как long (например, «воскресенье») и short (сокращенно, т.е. «солнце») названия. Таким образом, мы можем использовать «короткое» имя, когда в календаре мало места:

Intl.DateTimeFormat([locale], { weekday: 'long' })
Intl.DateTimeFormat([locale], { weekday: 'short' })

Давайте создадим небольшой вспомогательный метод, который немного упростит вызов каждого из них:

const weekdays = (firstDay, locale) => { const date = new Date(0); const arr = [...Array(7).keys()].map(i => { date.setDate(5 + i) return { long: new Intl.DateTimeFormat([locale], { weekday: 'long'}).format(date), short: new Intl.DateTimeFormat([locale], { weekday: 'short'}).format(date) } }) for (let i = 0; i < 8 - firstDay; i++) arr.splice(0, 0, arr.pop()); return arr;
}

Вот как мы вызываем это в шаблоне:

<ol> ${weekdays(config.info.firstDay,locale).map(name => ` <li> <abbr title="${name.long}">${name.short}</abbr> </li>`).join('') }
</ol>

Номера дней

И, наконец, дни, завернутые в <ol> элемент:

${[...Array(numOfDays).keys()].map(i => { const cur = new Date(year, month, i + 1); let day = cur.getDay(); if (day === 0) day = 7; const today = renderToday && (config.today.day === i + 1) ? ' data-today':''; return ` <li data-day="${day}"${today}${i === 0 || day === config.info.firstDay ? ` data-weeknumber="${new Intl.NumberFormat(locale).format(getWeek(cur))}"`:''}${config.info.weekend.includes(day) ? ` data-weekend`:''}> <time datetime="${year}-${(pad(month))}-${pad(i)}" tabindex="0"> ${new Intl.NumberFormat(locale).format(i + 1)} </time> </li>`
}).join('')}

Давайте разберемся с этим:

  1. Мы создаем «фиктивный» массив на основе переменной «количество дней», которую мы будем использовать для итерации.
  2. Мы создаем day переменная для текущего дня в итерации.
  3. Устраняем несоответствие между Intl.Locale API и getDay().
  4. Если же линия индикатора day равно today, мы добавляем data-* атрибутов.
  5. Наконец, мы возвращаем <li> элемент в виде строки с объединенными данными.
  6. tabindex="0" делает элемент доступным для фокусировки при использовании навигации с помощью клавиатуры после любых положительных значений tabindex (Примечание: вы должны никогда Добавить положительный tabindex-значения)

к «дополнить» числа в datetime атрибут, мы используем небольшой вспомогательный метод:

const pad = (val) => (val + 1).toString().padStart(2, '0');

Номер недели

Опять же, «номер недели» — это место, где неделя приходится на 52-недельный календарь. Для этого мы также используем небольшой вспомогательный метод:

function getWeek(cur) { const date = new Date(cur.getTime()); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); const week = new Date(date.getFullYear(), 0, 4); return 1 + Math.round(((date.getTime() - week.getTime()) / 86400000 - 3 + (week.getDay() + 6) % 7) / 7);
}

я этого не писал getWeek-метод. Это очищенная версия этот скрипт.

Вот и все! Благодаря Intl.Locale, Intl.DateTimeFormat и Intl.NumberFormat API, теперь мы можем просто изменить lang-атрибут <html> элемент для изменения контекста календаря в зависимости от текущего региона:

Календарная сетка на январь 2023 года.
de-DE
Календарная сетка на январь 2023 года.
fa-IR
Календарная сетка на январь 2023 года.
zh-Hans-CN-u-nu-hanidec

Стиль календаря

Вы можете вспомнить, как все дни всего лишь один <ol> с элементами списка. Чтобы оформить их в удобочитаемый календарь, мы погрузимся в удивительный мир CSS Grid. На самом деле, мы можем перепрофилировать ту же сетку из стартовый шаблон календаря прямо здесь, на CSS-Tricks, но немного обновил :is() реляционное псевдо для оптимизации кода.

Обратите внимание, что по пути я определяю настраиваемые переменные CSS (и добавляю к ним префикс ---kalel- во избежание конфликтов).

kal-el :is(ol, ul) { display: grid; font-size: var(--kalel-fz, small); grid-row-gap: var(--kalel-row-gap, .33em); grid-template-columns: var(--kalel-gtc, repeat(7, 1fr)); list-style: none; margin: unset; padding: unset; position: relative;
}
Календарная сетка из семи столбцов с показанными линиями сетки.

Давайте нарисуем границы вокруг чисел даты, чтобы визуально разделить их:

kal-el :is(ol, ul) li { border-color: var(--kalel-li-bdc, hsl(0, 0%, 80%)); border-style: var(--kalel-li-bds, solid); border-width: var(--kalel-li-bdw, 0 0 1px 0); grid-column: var(--kalel-li-gc, initial); text-align: var(--kalel-li-tal, end); }

Сетка из семи столбцов отлично работает, когда первый день месяца причислены первый день недели для выбранного региона). Но это скорее исключение, чем правило. В большинстве случаев нам нужно перенести первый день месяца на другой день недели.

Отображение первого дня месяца, приходящегося на четверг.

Помните обо всех доп. data-* атрибуты, которые мы определили при написании нашей разметки? Мы можем подключиться к ним, чтобы обновить столбец сетки (--kalel-li-gc) номер первого числа месяца ставится на:

[data-firstday="1"] [data-day="3"]:first-child { --kalel-li-gc: 1 / 4;
}

В этом случае мы переходим от первого столбца сетки к четвертому столбцу сетки, что автоматически «подталкивает» следующий элемент (День 2) к пятому столбцу сетки и так далее.

Давайте добавим немного стиля к «текущей» дате, чтобы она выделялась. Это только мои стили. Вы можете полностью делать то, что вы хотели бы здесь.

[data-today] { --kalel-day-bdrs: 50%; --kalel-day-bg: hsl(0, 86%, 40%); --kalel-day-hover-bgc: hsl(0, 86%, 70%); --kalel-day-c: #fff;
}

Мне нравится идея стилизовать номера дат для выходных иначе, чем для будних дней. Я собираюсь использовать красноватый цвет, чтобы стилизовать их. Обратите внимание, что мы можем достичь :not() псевдокласс, чтобы выбрать их, оставив только текущую дату:

[data-weekend]:not([data-today]) { --kalel-day-c: var(--kalel-weekend-c, hsl(0, 86%, 46%));
}

О, и давайте не будем забывать номера недель, которые идут перед номером первой даты каждой недели. Мы использовали data-weeknumber атрибут в разметке для этого, но числа не будут отображаться, если мы не покажем их с помощью CSS, что мы можем сделать на ::before псевдоэлемент:

[data-weeknumber]::before { display: var(--kalel-weeknumber-d, inline-block); content: attr(data-weeknumber); position: absolute; inset-inline-start: 0; /* additional styles */
}

На этом мы технически закончили! Мы можем визуализировать сетку календаря, которая показывает даты текущего месяца, с учетом локализации данных по локали и обеспечения правильной семантики календаря. И все, что мы использовали, это ванильный JavaScript и CSS!

Но давайте возьмем это Еще один шаг...

Отрисовка всего года

Возможно, вам нужно отобразить даты за весь год! Таким образом, вместо того, чтобы отображать текущий месяц, вы можете отобразить все сетки месяцев для текущего года.

Хорошая вещь в подходе, который мы используем, заключается в том, что мы можем вызывать render метод столько раз, сколько мы хотим, и просто меняем целое число, которое идентифицирует месяц в каждом экземпляре. Давайте назовем это 12 раз, исходя из текущего года.

так же просто, как позвонить в render-метод 12 раз и просто измените целое число для month - i:

[...Array(12).keys()].map(i => render( new Date(date.getFullYear(), i, date.getDate()), config.locale, date.getMonth() )
).join('')

Вероятно, хорошей идеей будет создать новую родительскую оболочку для отображаемого года. Каждая календарная сетка представляет собой <kal-el> элемент. Давайте вызовем новую родительскую оболочку <jor-el>, Где Джор-Эл — имя отца Кал-Эла..

<jor-el id="app" data-year="true"> <kal-el data-firstday="7"> <!-- etc. --> </kal-el> <!-- other months -->
</jor-el>

Мы можем использовать <jor-el> чтобы создать сетку для наших сеток. Итак, мета!

jor-el { background: var(--jorel-bg, none); display: var(--jorel-d, grid); gap: var(--jorel-gap, 2.5rem); grid-template-columns: var(--jorel-gtc, repeat(auto-fill, minmax(320px, 1fr))); padding: var(--jorel-p, 0);
}

Финальная демонстрация

Бонус: календарь конфетти

Я прочитал прекрасную книгу под названием Создание и разрушение сетки на днях и наткнулась на вот этот красивый «новогодний плакат»:

Источник: Создание и разрушение сети (2-е издание) Тимоти Самара

Я подумал, что мы могли бы сделать что-то подобное, ничего не меняя в HTML или JavaScript. Я позволил себе включить полные названия месяцев и числа вместо названий дней, чтобы сделать их более читабельными. Наслаждаться!

Отметка времени:

Больше от CSS хитрости