Введение
У карточек настоящих фотографий Moonroom уже есть читаемые подписи и адаптивная компоновка. Подробный просмотрщик пока не подключён. Ваша задача — открывать из каждой карточки её собственную большую фотографию, позволять посетителям перемещаться по коллекции и возвращаться точно к той карточке, которую они использовали. Вы также создадите и устраните настоящую ошибку отсутствующего изображения, не подменяя его другим.
Эта работа начинается со своей полной галереи и локальных фотографий. Она не зависит от файлов предыдущего окружения. Нужны отношения между карточками и данными из первой работы, функции JavaScript, выбор элементов DOM и события. Новые механизмы поиска в массиве, диалога, фокуса и событий изображения объясняются перед использованием. Оценка автора для чтения и практики — 60 минут.
После открытия или обновления страницы сайта дождитесь завершения загрузки, прежде чем активировать элементы управления фотографиями. Используйте WebIDE для /home/labex/project/moonroom и Web 8080 для сайта. Редактируйте viewer.js; временное изменение пути large в photos.js допускается только для явно описанного упражнения с ошибкой и должно быть отменено до проверки. Сохраните остальные предоставленные файлы, метаданные и оригинальные изображения. Две записи Earthrise остаются отдельными, а все карточки, миниатюры и подписи — доступными для использования. Можно реализовать эквивалентное поведение JavaScript; сохраните заданные идентификаторы кнопок, стабильные атрибуты связи фотографий, видимые элементы Close, Previous и Next, а также связи заголовка и описания встроенного диалога. Эти клавиатурные упражнения не являются полной сертификацией доступности.
Откройте Нужную Фотографию и Вернитесь к Её Карточке
На этом шаге вы соедините существующие карточки со встроенным модальным диалогом, включая надёжную точку возврата клавиатурного фокуса.
Переключитесь в Web 8080, выберите ссылку сайта Study collection и откройте и закройте фотографию клавишами Tab, Enter и Esc. Затем выберите Current exhibition. Три карточки с подписями уже присутствуют. Попробуйте Enter на карточке: пока ничего не произойдёт, потому что ваш viewer.js содержит только комментарий. Это ожидаемое незавершённое поведение, а не неисправность окружения.
Переключитесь в WebIDE, откройте папку moonroom в Explorer и прочитайте index.html. Элемент dialog — встроенная панель браузера. Его aria-labelledby="photo-title" указывает на видимый заголовок, а aria-describedby="photo-caption" — на текущее пояснение. showModal() открывает его в модальном режиме: остальное содержимое страницы становится неактивным, пока он открыт. Браузер обеспечивает базовое поведение Tab в модальном окне и закрытие по Esc; писать собственный механизм удержания фокуса не нужно. Не добавляйте tabindex самому диалогу. Подготовленная кнопка Close имеет autofocus, поэтому открытие панели передаёт фокус очевидному способу выхода.
Откройте viewer.js. const хранит ссылки на элементы DOM. let используется для изменяемых значений: currentIndex — выбранная позиция массива, начиная с нуля, а openingButton запоминает настоящую кнопку, открывшую панель. photos[currentIndex] извлекает полную запись фотографии. showPhoto() совместно обновляет её заголовок, подпись, альтернативный текст и настоящее большое изображение.
У галереи один обработчик щелчка. Enter на встроенной кнопке тоже активирует её. event.target — точный элемент, по которому щёлкнули, возможно изображение или заголовок внутри кнопки. closest('button[data-photo-id]') ищет снаружи соответствующую кнопку открытия. if (!button) return игнорирует щелчки, не пришедшие от такой кнопки. Это делегирование событий: один обработчик обслуживает все существующие кнопки карточек.
findIndex() ищет в массиве и возвращает позицию, идентификатор которой совпадает с dataset.photoId кнопки. Функция обратного вызова возвращает true, когда photo.id === button.dataset.photoId; === сравнивает без преобразования типов значений. Если запись не найдена, findIndex() возвращает -1, поэтому защитное условие прерывает обработку вместо открытия неверной фотографии. Мы намеренно сравниваем идентификаторы, а не повторяющийся заголовок Earthrise.
Событие close диалога срабатывает и при закрытии по Esc, и по кнопке Close. openingButton.focus() возвращает фокус к запомненной карточке. Такое применение focus() реализует возврат; при ручном тесте вы всё равно должны достигать карточек и активировать их настоящей клавиатурой.
Замените всё содержимое viewer.js полным кодом ниже и сохраните через Ctrl+S или Cmd+S:
const viewer = document.querySelector('#viewer');
const largePhoto = document.querySelector('#large-photo');
const photoTitle = document.querySelector('#photo-title');
const photoCaption = document.querySelector('#photo-caption');
let currentIndex = 0;
let openingButton;
function showPhoto() {
const photo = photos[currentIndex];
photoTitle.textContent = photo.title;
photoCaption.textContent = photo.caption;
largePhoto.alt = photo.alt;
largePhoto.src = photo.large;
}
document.querySelector('#gallery').addEventListener('click', function (event) {
const button = event.target.closest('button[data-photo-id]');
if (!button) return;
currentIndex = photos.findIndex(function (photo) {
return photo.id === button.dataset.photoId;
});
if (currentIndex === -1) return;
openingButton = button;
showPhoto();
viewer.showModal();
});
document.querySelector('#close').addEventListener('click', function () {
viewer.close();
});
viewer.addEventListener('close', function () {
openingButton.focus();
});
Переключитесь в Web 8080, при необходимости выберите Current exhibition и обновите страницу. Достигайте каждой карточки клавишей Tab и открывайте Enter. Первый Earthrise должен показывать фотографию и подпись Apollo 8, последний — Apollo 11. После каждого нажимайте Esc и наблюдайте фокус на той же карточке. Снова откройте фото и используйте Tab, затем Enter на Close как второй способ закрытия. Previous и Next будут подключены только на следующем шаге.
Если панель не открывается, вернитесь в WebIDE и сравните файл с полным блоком. Проверьте сохранение под именем viewer.js, наличие строки скрипта в index.html и совпадение значений data-photo-id с photos.js. Исправив опечатку, сохраните и обновите сайт. Если показана неверная сцена, сравните вместе large, заголовок и подпись текущей записи; не меняйте исходные данные, чтобы скрыть ошибку поиска по идентификатору. Запускайте проверку после того, как заработают все три независимых пути Enter/Esc.
Просмотрите Всю Коллекцию и Её Границы
На этом шаге вы будете обновлять тот же подробный вид, не теряя исходную карточку, из которой он был открыт.
Вернитесь в WebIDE и сравните позиции массива с отсчётом от нуля в photos.js: первая — 0, вторая — 1, последняя — photos.length - 1. length считает записи. Видимая позиция должна считаться от единицы, поэтому currentIndex + 1 даёт понятное человеку число, например 1 / 3.
Свойство disabled принимает логическое значение. previous.disabled = currentIndex === 0 отключает Previous только на первой фотографии; соответствующее правило Next отключает её только на последней. Эти границы предотвращают обращение за пределы массива. Обработчики щелчка также проверяют границы перед изменением индекса. currentIndex -= 1 вычитает единицу и сохраняет новый индекс; += 1 прибавляет единицу. Каждое изменение вызывает showPhoto(), чтобы изображение, описание и позиция оставались синхронными.
Только открытие карточки меняет openingButton. Переход к другой фотографии внутри диалога не должен менять его: закрытие всё равно возвращает к карточке, с которой начался просмотр, даже если сейчас показана другая фотография.
Замените всё содержимое viewer.js этой полной версией и сохраните. Она сохраняет открытие и закрытие, добавляя навигацию с границами:
const viewer = document.querySelector('#viewer');
const largePhoto = document.querySelector('#large-photo');
const photoTitle = document.querySelector('#photo-title');
const photoCaption = document.querySelector('#photo-caption');
const previous = document.querySelector('#previous');
const next = document.querySelector('#next');
let currentIndex = 0;
let openingButton;
function showPhoto() {
const photo = photos[currentIndex];
photoTitle.textContent = photo.title;
photoCaption.textContent = photo.caption;
largePhoto.alt = photo.alt;
largePhoto.src = photo.large;
previous.disabled = currentIndex === 0;
next.disabled = currentIndex === photos.length - 1;
document.querySelector('#photo-position').textContent = (currentIndex + 1) + ' / ' + photos.length;
}
document.querySelector('#gallery').addEventListener('click', function (event) {
const button = event.target.closest('button[data-photo-id]');
if (!button) return;
currentIndex = photos.findIndex(function (photo) {
return photo.id === button.dataset.photoId;
});
if (currentIndex === -1) return;
openingButton = button;
showPhoto();
viewer.showModal();
});
previous.addEventListener('click', function () {
if (currentIndex > 0) {
currentIndex -= 1;
showPhoto();
}
});
next.addEventListener('click', function () {
if (currentIndex < photos.length - 1) {
currentIndex += 1;
showPhoto();
}
});
document.querySelector('#close').addEventListener('click', function () {
viewer.close();
});
viewer.addEventListener('close', function () {
openingButton.focus();
});
Переключитесь в Web 8080 и обновите Current exhibition. Откройте первый Earthrise с клавиатуры. Previous отключена; Tab пропускает эту кнопку. Перейдите клавишей Tab к Next и нажмите Enter. Вы должны увидеть вертикальный след ботинка, его заголовок и подпись и 2 / 3. Продолжите до последнего Earthrise и убедитесь, что появились его изображение и подпись Apollo 11, а Next отключена.
Перейдите Tab к Previous и вернитесь через обе фотографии. Удерживайте Shift при нажатии Tab, чтобы двигаться назад между доступными элементами управления; это перемещает клавиатурный фокус, тогда как активация Previous меняет фотографию. Это разные действия. Нажмите Esc и убедитесь, что фокус вернулся к исходной первой карточке. Теперь откройте среднюю карточку напрямую, перейдите к другой фотографии и закройте: фокус должен вернуться к средней карточке.
Отключённый элемент на первой или последней границе ожидаем. Элемент, который выглядит активным, но ничего не делает, — нет. Если заголовок меняется, а фотография нет, вернитесь в WebIDE и убедитесь, что каждый переход вызывает одну и ту же функцию showPhoto(). Если Esc всегда возвращает к первой карточке, проверьте, что openingButton присваивается в обработчике открытия карточки и не перезаписывается обработчиками навигации. При необходимости восстановите полный код, сохраните и обновите страницу перед проверкой.
Честно Сообщите об Отсутствующем Изображении и Обеспечьте Восстановление
На этом шаге вы обработаете настоящий сбой загрузки изображения, сохраняя позицию посетителя и работоспособность остальной коллекции.
Событие load изображения означает успешную загрузку его файла; error означает, что изображение не удалось загрузить, например из-за отсутствующего пути. Это события изображения браузера, а не доказательство, что произвольный HTTP-ответ содержит корректную картинку. Сбой соединения с сервером может сделать недоступной всю страницу; здесь страница работает, а запрос одной картинки завершается ошибкой.
В WebIDE откройте viewer.js. Эта версия назначает обработчики событий до присваивания src, чтобы быстрая картинка из кеша не завершилась до появления обработчиков. Она скрывает старую фотографию во время загрузки и показывает Loading photo... в существующем абзаце с role="status". При успехе показывает настоящее изображение и очищает сообщение. При ошибке оставляет картинку скрытой и выводит понятные инструкции по восстановлению. Правило CSS dialog img[hidden] { display: none; } необходимо, потому что обычное правило изображения диалога использует display: block. Иначе одного атрибута hidden недостаточно: правило CSS его перекроет.
Обработчик ошибки не меняет src на миниатюру или другую фотографию. Такая подмена скрыла бы сбой и показала содержимое, не соответствующее текущей записи. Пользователь всё ещё может закрыть панель или перейти к неповреждённой фотографии.
Замените всё содержимое viewer.js полной окончательной версией и сохраните:
const viewer = document.querySelector('#viewer');
const largePhoto = document.querySelector('#large-photo');
const photoTitle = document.querySelector('#photo-title');
const photoCaption = document.querySelector('#photo-caption');
const photoError = document.querySelector('#photo-error');
const previous = document.querySelector('#previous');
const next = document.querySelector('#next');
let currentIndex = 0;
let openingButton;
function showPhoto() {
const photo = photos[currentIndex];
photoTitle.textContent = photo.title;
photoCaption.textContent = photo.caption;
largePhoto.alt = photo.alt;
photoError.textContent = 'Loading photo...';
largePhoto.hidden = true;
largePhoto.onload = function () {
largePhoto.hidden = false;
photoError.textContent = '';
};
largePhoto.onerror = function () {
largePhoto.hidden = true;
photoError.textContent = 'This photo could not be loaded. Close the viewer and check its large image path.';
};
largePhoto.src = photo.large;
previous.disabled = currentIndex === 0;
next.disabled = currentIndex === photos.length - 1;
document.querySelector('#photo-position').textContent = (currentIndex + 1) + ' / ' + photos.length;
}
document.querySelector('#gallery').addEventListener('click', function (event) {
const button = event.target.closest('button[data-photo-id]');
if (!button) return;
currentIndex = photos.findIndex(function (photo) {
return photo.id === button.dataset.photoId;
});
if (currentIndex === -1) return;
openingButton = button;
showPhoto();
viewer.showModal();
});
previous.addEventListener('click', function () {
if (currentIndex > 0) {
currentIndex -= 1;
showPhoto();
}
});
next.addEventListener('click', function () {
if (currentIndex < photos.length - 1) {
currentIndex += 1;
showPhoto();
}
});
document.querySelector('#close').addEventListener('click', function () {
viewer.close();
});
viewer.addEventListener('close', function () {
openingButton.focus();
});
Теперь создайте контролируемую ошибку пути в своих файлах и исправьте её. Откройте photos.js в WebIDE. В записи с id footprint измените только значение large с images/footprint.jpg на images/missing-photo.jpg. Не меняйте миниатюру, идентификатор, подпись или альтернативный текст. Сохраните файл.
Переключитесь в Web 8080, обновите Current exhibition и откройте A trace of a visitor через Tab и Enter. Миниатюра по-прежнему работает, но запрос большого изображения теперь завершится ошибкой. Ожидается видимый текст This photo could not be loaded. Close the viewer and check its large image path. Панель не должна показывать Earthrise или растянутую миниатюру в качестве замены. Заголовок и подпись следа ботинка сохраняются, а Close, Previous и Next остаются доступными.

Панель со сбоем сохраняет заголовок и подпись следа ботинка, без изображения-замены. Next остаётся доступной, чтобы посетитель мог продолжить.
Используйте Tab и Enter на Next. Неповреждённое изображение Apollo 11 должно загрузиться, а сообщение об ошибке — очиститься. Нажмите Esc и убедитесь, что фокус вернулся к карточке следа, из которой панель была открыта. Это восстановление просмотра внутри коллекции; неверный путь к файлу оно не исправило.
Вернитесь в WebIDE, восстановите точное значение large записи footprint на images/footprint.jpg и сохраните. Переключитесь в Web 8080 и обновите Current exhibition, чтобы браузер прочитал восстановленные данные. Снова откройте след ботинка. Теперь его настоящая вертикальная фотография должна декодироваться и появиться без сообщения об ошибке. Обновление браузера перечитывает данные страницы, но не сбрасывает сохранённую работу и не создаёт окружение заново.

После восстановления пути и обновления вертикальный след ботинка отображается в просмотрщике без растяжения; сообщение об ошибке исчезло.
Наконец, снова проверьте все три карточки, границы в обоих направлениях и оба способа закрытия. Сузьте панель сайта и убедитесь, что вся фотография и заголовок с переносами помещаются в диалоге без горизонтальной прокрутки страницы. Все исходные миниатюры, метаданные, изображения и сведения об источниках должны сохраниться. Итоговая проверка требует восстановить предоставленные данные и испытывает обработку сбоя только в одноразовой копии ваших файлов; она никогда не удаляет изображение в вашей настоящей выставке.
Резюме
Вы использовали стабильные идентификаторы, чтобы показывать настоящее содержимое каждой фотографии во встроенном диалоге. Вы добавили навигацию назад и вперёд с границами, сохранили исходную точку возврата для Esc и Close и обработали настоящий запрос отсутствующего изображения, не заменяя его другим фото. Галерея сохраняет сетку, подписи и остальные фотографии доступными, честно сообщая о локальном сбое.
