GrImageViewer
Берут, когда картинку нужно рассмотреть.
Обзор
Когда брать
- картинку нужно рассмотреть — скан документа, чертёж, фотография товара: масштаб, поворот и панорамирование уже есть;
- изображений несколько —
urlListлистается стрелками и свайпом, индекс управляем черезv-model; - исходник можно скачать —
showDownloadдаёт кнопку рядом с масштабом; - работа идёт с клавиатуры — весь набор действий имеет клавиши и подписи.
Когда взять другое
| Нужно | Берите |
|---|---|
| Показать изображение в потоке страницы | обычный <img> или GrCard |
| Аватар или миниатюра | GrAvatar |
| Своя раскладка поверх страницы | GrModal |
| Выбрать и загрузить файл | GrFileUpload |
| Галерея-карусель в потоке страницы | GrCarousel |
Playground контролы выводятся из API, а не пишутся руками
Окно откроется поверх страницы с пропами, заданными справа.
0 изменено· состояние — в адресе
<GrImageViewer
:urlList="urlList"
showProgress
v-model="value"
/>Примеры 3 у каждого — живое демо и его код
Фотоотчёт выезда мастера
initial-indexоткрывает просмотр на том фото, по которому кликнули,show-progressговорит, сколько их всего, аshow-downloadотдаёт оригинал. У каждого кадра свойalt— «провода вразброс поверх автоматов», — потому что придумать описание просмотрщик не может, а имя файла им не является.Visit #5523 · Harbour Road office, cabinet B2Technician Mina Ortiz · Tue 6 Oct, 14:20–15:05 План этажа, который читают в восемь раз крупнее
Номера столов на плане мелкие, и прочитать их помогает просмотрщик:
max-scaleподнимает предел до восьми,zoom-rateделает шаг крупнее, аdraggableпозволяет двигать даже вписанный план. Колесо увеличивает в точку под курсором, а «1:1» ведёт к реальным пикселям, а не к «вписанному».Your desk tomorrow: 4.17Level 4, east wing · two rows from the lifts
Проверка сканов документов
Вторая страница пришла боком: поворот встроен, а своя кнопка проверяющего встаёт рядом через
toolbar-actions, не заменяя тулбар. Слот отдаёт кадр — индекс,alt, поворот, — поэтому «Запросить новый скан» называет страницу, о которой речь.Identity check · Jonas Weber2 pages uploaded today
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
ariaLabel | string | undefined | undefined | Доступное имя слоя. Модальный диалог без имени — нарушение `aria-dialog-name`: диктор объявит «диалог» и замолчит. |
prevLabel | string | undefined | undefined | i18n: aria-label кнопки «предыдущее изображение». |
nextLabel | string | undefined | undefined | i18n: aria-label кнопки «следующее изображение». |
emptyText | string | undefined | undefined | i18n: текст в пустом состоянии (нет изображений). |
closeLabel | string | undefined | undefined | i18n: aria-label кнопки закрытия. |
showProgress | boolean | undefined | false | Счётчик «3 / 12» в шапке. По умолчанию `false`. |
initialIndex | number | undefined | 0 | Кадр, с которого открывается просмотрщик; смена пропа у открытого переключает кадр. По умолчанию `0`. |
zoomRate | number | undefined | 1.2 | Множитель одного шага зума — кнопками и с клавиатуры. По умолчанию `1.2`. |
minScale | number | undefined | 0.5 | Нижний предел масштаба; `1` — кадр вписан в окно. По умолчанию `0.5`. |
maxScale | number | undefined | 5 | Верхний предел масштаба относительно вписанного кадра. По умолчанию `5`. |
hideOnClickModal | boolean | undefined | false | Закрывать по клику на подложку вокруг кадра. По умолчанию `false`. |
closeOnPressEscape | boolean | undefined | true | Закрывать по `Esc`; открытый поверх модалки закрывает себя, а не её. По умолчанию `true`. |
showZoomValue | boolean | undefined | true | Масштаб в процентах в шапке; 100% — кадр вписан в окно. По умолчанию `true`. |
wheelZoom | boolean | undefined | true | Включает масштабирование колесом мыши / жестом на трекпаде. По умолчанию включено. |
draggable | boolean | undefined | false | Включает перетаскивание (pan) картинки мышью. При наведении курсор «рука». По умолчанию выключено. |
zIndexVar | string | undefined | undefined | Имя CSS-переменной слоя — escape-hatch мимо `--gr-z-modal`. Сырое число компонент не принимает: слой задаётся шкалой, см. `docs/z-index.md`. |
zoomInLabel | string | undefined | undefined | i18n: aria-label кнопки «увеличить». |
zoomOutLabel | string | undefined | undefined | i18n: aria-label кнопки «уменьшить». |
resetZoomLabel | string | undefined | undefined | i18n: aria-label кнопки «сбросить масштаб». |
zoomToNaturalLabel | string | undefined | undefined | i18n: aria-label кнопки «до натурального размера». |
rotateLeftLabel | string | undefined | undefined | i18n: aria-label кнопки «повернуть влево». |
rotateRightLabel | string | undefined | undefined | i18n: aria-label кнопки «повернуть вправо». |
showDownload | boolean | undefined | false | Показывать кнопку скачивания текущего кадра. |
downloadLabel | string | undefined | undefined | i18n: aria-label кнопки «скачать». |
modelValueобязательныйv-model | boolean | — | Открыт ли просмотрщик (`v-model`). Закрывается он сам — крестиком, `Esc` или подложкой. |
urlListобязательный | GrImageViewerSource[] | — | Кадры. Строка — только адрес; объект `{ src, alt }` даёт изображению альтернативный текст: без него просмотрщик пуст для незрячего пользователя, а придумать текст за потребителя компонент не может. |
Слоты
| Slot | Type | Описание |
|---|---|---|
toolbar | GrImageViewerSlotProps | Панель инструментов целиком вместо встроенной. |
toolbar-actions | GrImageViewerSlotProps | Свои кнопки рядом со встроенными — поворот, скачивание, печать. |
События
| Event | Сигнатура | Описание |
|---|---|---|
close | (event: "close"): void | — |
update:modelValue | (event: "update:modelValue", value: boolean): void | — |
change | (event: "change", newIndex: number): void | — |
rotate | (event: "rotate", deg: number): void | — |
download | (event: "download", payload: { src: string; alt: string; index: number; }): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
close | () => void | Закрыть просмотрщик, как крестиком: шлёт `update:modelValue` и `close`. |
prev | () => void | Предыдущий кадр; с первого переходит на последний. |
next | () => void | Следующий кадр; с последнего переходит на первый. |
zoomIn | () => void | Увеличить в `zoomRate` раз, не выше `maxScale`. |
zoomOut | () => void | Уменьшить в `zoomRate` раз, не ниже `minScale`. |
zoomToNatural | () => void | Масштаб «один к одному»: реальные 100%, а не номинальные. |
reset | () => void | Вернуть кадру исходный вид: масштаб, поворот и сдвиг. |
rotateLeft | () => void | Повернуть на 90° против часовой стрелки. |
rotateRight | () => void | Повернуть на 90° по часовой стрелке. |
download | () => void | Скачать текущий кадр — то же, что делает кнопка тулбара. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrImageViewerItem, GrImageViewerSource, GrImageViewerProps } from '@feugene/granularity/components/GrImageViewer'Файлdist/types/components/GrImageViewer/GrImageViewer.vue.d.ts
Кадр просмотрщика: только адрес или адрес с альтернативным текстом.
type GrImageViewerItem = {
src: string;
alt?: string;
};type GrImageViewerSource = string | GrImageViewerItem;interface GrImageViewerProps {
/** Открыт ли просмотрщик (`v-model`). Закрывается он сам — крестиком, `Esc` или подложкой. */
modelValue: boolean;
/**
* Кадры. Строка — только адрес; объект `{ src, alt }` даёт изображению
* альтернативный текст: без него просмотрщик пуст для незрячего
* пользователя, а придумать текст за потребителя компонент не может.
*/
urlList: GrImageViewerSource[];
/**
* Кадр, с которого открывается просмотрщик; смена пропа у открытого переключает кадр.
* По умолчанию `0`.
*/
initialIndex?: number;
/** Множитель одного шага зума — кнопками и с клавиатуры. По умолчанию `1.2`. */
zoomRate?: number;
/** Нижний предел масштаба; `1` — кадр вписан в окно. По умолчанию `0.5`. */
minScale?: number;
/** Верхний предел масштаба относительно вписанного кадра. По умолчанию `5`. */
maxScale?: number;
/** Закрывать по клику на подложку вокруг кадра. По умолчанию `false`. */
hideOnClickModal?: boolean;
/** Закрывать по `Esc`; открытый поверх модалки закрывает себя, а не её. По умолчанию `true`. */
closeOnPressEscape?: boolean;
/** Счётчик «3 / 12» в шапке. По умолчанию `false`. */
showProgress?: boolean;
/** Масштаб в процентах в шапке; 100% — кадр вписан в окно. По умолчанию `true`. */
showZoomValue?: boolean;
/** Включает масштабирование колесом мыши / жестом на трекпаде. По умолчанию включено. */
wheelZoom?: boolean;
/** Включает перетаскивание (pan) картинки мышью. При наведении курсор «рука». По умолчанию выключено. */
draggable?: boolean;
/**
* Имя CSS-переменной слоя — escape-hatch мимо `--gr-z-modal`. Сырое число
* компонент не принимает: слой задаётся шкалой, см. `docs/z-index.md`.
*/
zIndexVar?: string;
/** i18n: aria-label кнопки закрытия. */
closeLabel?: string;
/** i18n: aria-label кнопки «предыдущее изображение». */
prevLabel?: string;
/** i18n: aria-label кнопки «следующее изображение». */
nextLabel?: string;
/** i18n: aria-label кнопки «увеличить». */
zoomInLabel?: string;
/** i18n: aria-label кнопки «уменьшить». */
zoomOutLabel?: string;
/** i18n: aria-label кнопки «сбросить масштаб». */
resetZoomLabel?: string;
/** i18n: aria-label кнопки «до натурального размера». */
zoomToNaturalLabel?: string;
/** i18n: aria-label кнопки «повернуть влево». */
rotateLeftLabel?: string;
/** i18n: aria-label кнопки «повернуть вправо». */
rotateRightLabel?: string;
/** Показывать кнопку скачивания текущего кадра. */
showDownload?: boolean;
/** i18n: aria-label кнопки «скачать». */
downloadLabel?: string;
/** i18n: текст в пустом состоянии (нет изображений). */
emptyText?: string;
/**
* Доступное имя слоя. Модальный диалог без имени — нарушение
* `aria-dialog-name`: диктор объявит «диалог» и замолчит.
*/
ariaLabel?: string;
}interface GrImageViewerEmits {
/** Просмотрщик просит закрыться (`v-model`): приходит только с `false`. */
(e: 'update:modelValue', value: boolean): void;
/**
* Закрыт изнутри — крестиком, `Esc`, подложкой или методом `close`. Внешний сброс модели
* его не шлёт.
*/
(e: 'close'): void;
/** Показан другой кадр. */
(e: 'change', newIndex: number): void;
/** Кадр повёрнут; payload — накопленный угол в градусах с шагом 90, без приведения к 0–360. */
(e: 'rotate', deg: number): void;
/** Нажата кнопка скачивания. Само скачивание компонент уже запустил. */
(e: 'download', payload: {
src: string;
alt: string;
index: number;
}): void;
}Файлdist/types/components/GrImageViewer/config.d.ts
const grImageViewerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrImageViewer">;Файлdist/types/components/GrImageViewer/safelist.d.ts
const grImageViewerSafelist: string[];Файлdist/types/components/GrImageViewer/index.d.ts
type GrImageViewerInstance = ComponentExposed<typeof GrImageViewerComponent>;Доступность
Клавиши
Esc
| Клавиша | Действие |
|---|---|
| Esc | закрыть |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Кадры: строка или { src, alt }
urlList принимает оба варианта, в том числе вперемешку:
<GrImageViewer
v-model="open"
:url-list="[
{ src: '/photos/roof.jpg', alt: 'Крыша с высоты' },
'/photos/plan.png',
]"
/>Строка задаёт только адрес — alt у такого кадра пустой. Придумать текст за
потребителя компонент не может, но и молча подставлять имя файла не должен:
для незрячего пользователя «roof.jpg» не описание. Объектная форма — способ
дать описание там, где оно есть.
alt приходит и в слот тулбара (src, alt в slot-props) — тулбар может
показать подпись рядом с кадром.
Доступное имя и объявление смены кадра
Слой объявляет себя диалогом, поэтому у него всегда есть имя: из локали
(gr.imageViewer.label) или из пропа ariaLabel. Диалог без имени — нарушение
aria-dialog-name.
Смена кадра уходит в общий живой регион пакета (announcer.md):
«Изображение 2 из 5» (gr.imageViewer.position). При открытии позиция не
объявляется — её скажет имя диалога.
Регион лежит вне просмотрщика, хотя тот накрывает страницу inert. Это
работает благодаря пропуску по data-gr-live-region в стеке слоёв: inert
выбрасывает поддерево из дерева доступности, и регион внутри него замолчал бы.
Счётчик showProgress остаётся чисто визуальным: он дублирует ту же
информацию глазами.
Зум, панорамирование и жесты
Колесо и жест трекпада увеличивают в точку под курсором, а не от центра: иначе до нужного угла увеличенного кадра пришлось бы добираться отдельным перетаскиванием.
Смещение кадра ограничено его собственным переполнением — насколько картинка вылезла за область просмотра, настолько её и можно тянуть. Утащить изображение в пустоту и потерять его нельзя, а при возврате к масштабу 1 смещение обнуляется само. Поворот на 90° меняет оси местами, и границы считаются по повёрнутому кадру.
Тянуть увеличенный кадр можно всегда — проп draggable разрешает
перетаскивание и на вписанном (актуально для повёрнутых кадров, которые вылезают
за экран и без зума).
У жестов два владельца, и это видно в поведении. Мышь и перо ведёт общий
примитив useDragGesture: слушатели на window, поэтому
отпускание ловится и за пределами картинки, и когда кадр сменился прямо посреди
жеста. Касание ведёт свой трекер — у него многопальцевая модель: pinch двумя
пальцами, свайп для листания, а на увеличенном кадре одиночный жест тянет.
Обрыв жеста (браузер забрал указатель — системный жест, звонок, потеря окна) заканчивает перетаскивание, оставляя кадр там, куда его довели, и не засчитывается как свайп: прерванный жест не листает.
Номинальный масштаб против реального
scale номинальный: единица — это «кадр вписан в окно» (object-contain), а не
натуральный размер. У фотографии 4752 px, вписанной в окно шириной ~1000 px,
номинальные 100% — это реальные 21%: пикселей на таком масштабе не видно вовсе.
Отсюда две разные кнопки тулбара. «100%» сбрасывает трансформацию и возвращает
вписанный кадр; «1:1» (actions.zoomToNatural) доводит до реальных 100% —
пиксель картинки на пиксель экрана. Считать это потребителю самому не нужно, хотя
данные для расчёта слот отдаёт (naturalWidth, renderedWidth, realScale).
Потолок maxScale действует и на «1:1»: он ограничивает зум сознательно, и
натуральный размер — не повод его обойти. Дефолтные maxScale: 5 для крупного
кадра до реальных 100% не дотягивают (нужное номинальное приближение — это
naturalWidth / renderedWidth при scale = 1, у фотографий это 5–12×), поэтому
крупным изображениям потолок задаётся пропом. Обратное тоже работает: если кадр
меньше своего места на экране, «1:1» уменьшит его — с оглядкой на minScale.
Сенсорные жесты работают на тех же pointer-событиях:
| Жест | Что делает |
|---|---|
| Два пальца | Масштаб по расстоянию между ними, якорь — середина |
| Палец на вписанном кадре, горизонтально | Листание кадров |
| Палец на увеличенном кадре | Перетаскивание |
Порог свайпа — 60px и преобладание горизонтали: вертикальное движение листать
не должно, пользователь целился не туда. Картинка объявлена touch-action: none, иначе браузер забирает жест себе и уводит страницу вместо зума.
Скачивание
showDownload добавляет в тулбар кнопку: она скачивает текущий кадр
(<a download>) и эмитит download с { src, alt, index } — для аналитики.
Кросс-доменный адрес браузер скачает не всегда: атрибут download для чужого
источника игнорируется, и файл откроется в новой вкладке. Там, где нужны
подписанные ссылки или свой запрос, кнопку выключают (showDownload: false) и
кладут свою в слот #toolbar-actions — он получает те же slot-props, включая
actions.download.
Предзагрузка соседних кадров
Соседи текущего кадра греются только у открытого просмотрщика: закрытый на странице не тянет два полноразмерных изображения просто потому, что он там есть. При смене кадра и при закрытии незавершённые загрузки обрываются — иначе быстрое перелистывание галереи копит запросы, которые уже никому не нужны.
Изменение списка на лету
Просмотрщик держится за кадр, а не за индекс. Догрузилась следующая
страница галереи — открытое изображение остаётся на экране вместе с масштабом
и поворотом, даже если по позиции оно сдвинулось. Пропал текущий кадр из
списка — индекс приводится к границам, а не к initialIndex: выбрасывать
пользователя на первое изображение при каждой подгрузке нельзя.
Слой
--gr-z-modal, как у остальных модальных оверлеев. zIndexVar подменяет
переменную слоя своей — тот же escape-hatch, что у useFloating и GrLoading.
Сырого числа компонент не принимает: слой задаётся шкалой, см.
../z-index.md.
Точное значение — calc(var(<токен>) + глубина): свою глубину среди открытых
модальных слоёв просмотрщик берёт из стека, поэтому поверх окна он оказывается
и по отрисовке, а не только по Esc. С zIndexVar это работает так же —
глубина прибавляется к подменённой переменной.
Esc, inert для нижних слоёв и возврат фокуса идут через общий стек
(useOverlayLayer), поэтому просмотрщик поверх модалки закрывает себя, а не её.
Токены хрома
Панель, кнопки и подложка красятся покомпонентными токенами:
| Токен | Что красит |
|---|---|
--gr-image-viewer-scrim | подложка под изображением |
--gr-image-viewer-chrome-bg | фон кнопок и панели инструментов |
--gr-image-viewer-chrome-bg-hover | фон кнопки под курсором |
--gr-image-viewer-chrome-bg-soft | мягкая подсветка кнопок тулбара |
--gr-image-viewer-chrome-fg | иконки и текст хрома |
--gr-image-viewer-chrome-fg-muted | текст пустого состояния |
--gr-image-viewer-chrome-brd | рамки и разделители |
--gr-image-viewer-ring | кольцо фокуса |
Значения одинаковы в светлой и тёмной теме намеренно: рядом с фотографией светлая панель спорит с содержимым и подсвечивает края кадра. Токены нужны не для смены темы, а чтобы потребитель мог перекрасить хром под свой продукт.
Императивный API
<GrImageViewer ref="viewer" v-model="open" :url-list="images" />viewer.value отдаёт close, prev, next, zoomIn, zoomOut,
zoomToNatural, reset, rotateLeft, rotateRight — тот же набор, что получает
слот тулбара.
Открытия среди них нет: оно принадлежит v-model, вторая точка входа
рассинхронизировала бы состояние с моделью.
События
| Событие | Когда |
|---|---|
update:modelValue | открытие/закрытие |
close | закрытие |
change | показан другой кадр, аргумент — новый индекс |
rotate | поворот, аргумент — накопленный угол в градусах |
Пропы этого примера