Эта страница переведена с английского. Читать страницу на английском
Публичные страницы, например магазин, показывают строки как карточки, а не как таблицу. CardGrid, Rail и SearchBox загружают свои строки через такое же действие (action), как у DataGrid: оно принимает DataQuery и возвращает Page. Разбиение на страницы, сортировка и текст поиска выполняются на сервере, а для чтения нужен доступ к действию, как и для DataGrid.
#[action(public)]
pub async fn catalog_products(q: DataQuery, store: ProductStore) -> Result<Page<Product>> {
Ok(q.apply(store.all()?.into_iter().filter(|p| p.active).collect()))
}Сетка карточек
CardGrid::of(catalog_products)
.title(t!("shop.products"))
.card(|p: &Product| {
Card::image(p.image.clone(), p.name.clone())
.text(p.price.to_string())
.badge(Badge::new(t!("shop.new")).tone(Tone::Success))
.link(shop_product, ProductPage { id: p.id })
})
.sort_option(t!("shop.sort.name"), Product::name, false)
.sort_option(t!("shop.sort.cheapest"), Product::price, false)
.image_size(300, 300)
.paging(Paging::infinite(24))- Карточки.
Card::image(image, title)делает одну карточку из строки, сtext,badge,altиlink(экран и его параметры; вся карточка это одна ссылка и открывается без перезагрузки страницы). Изображения ленивые, с шириной и высотой изimage_size, чтобы страница не прыгала. Альтернативный текст изображения пуст, потому что заголовок рядом называет карточку; задавайтеalt, когда картинка говорит больше. Адреса изображений, кроме пути,https:илиdata:image/, отбрасываются. - Дополнительно на карточке.
subtitleдобавляет под заголовком более бледную вторую строку (автор, категория).corner(badge)помещает значок над изображением в конечном углу, рядом сbadgeв начальном;badges([...])добавляет под текстом ряд значков, например теги.progress(fraction, label)рисует тонкую полосу вдоль нижнего края изображения: сколько уже сделано. Доля лежит в диапазоне от 0 до 1, а программы чтения с экрана произносят подпись и процент (role="progressbar"сaria-valuenow).horizontal()располагает карточку в строку: маленькое изображение в начале и текст рядом; сетка карточек или rail с такими карточками даёт им более широкие столбцы. Все это работает вCardGridиRail.
Card::image(c.cover.clone(), c.title.clone())
.subtitle(c.teacher.clone())
.corner(Badge::new(t!("courses.new")).tone(Tone::Success))
.badges(c.tags.iter().map(|t| Badge::new(t.clone())))
.progress(c.done, t!("courses.done"))
.horizontal()- Первая страница на сервере. Первая страница (24 карточки в сетке, 12 в rail или размер, заданный в
paging) входит в HTML страницы, поэтому поисковые системы и посетители без JavaScript видят карточки и их ссылки, а изображения несут свой размер, и при загрузке ничего не сдвигается. Следующие страницы загружает браузер. Публичная страница одинакова для всех посетителей, поэтому ее можно кешировать. Карточка, ссылающаяся на screen с маршрутом, ведет на его канонический адрес (/items/{slug}), а не на перенаправление. - Разбиение на страницы.
Paging::infinite(n)(по умолчанию, 24) показывает кнопку "Show more", которая также срабатывает, когда прокрутка доходит до неё; при нажатии кнопки фокус переходит на первую новую карточку.Paging::pages(n)показывает кнопки назад и вперёд и номер страницы. Размер страницы задаёт сервер; клиент лишь говорит, откуда продолжать. - Сортировка. Каждый
sort_optionэто порядок, который пользователи выбирают в списке select; первый это порядок, в котором карточки показываются сначала. Сервер принимает только эти порядки и отбрасывает фильтры и текст поиска, присланные клиентом. - Поиск.
.search(text)показывает только строки, подходящие под свободный текст (DataQuery::q), например текст с экрана результатов. - Маскированные поля. Карточки строятся замыканием, которое видит исходную строку. Для модели с полями
#[mask]карточки получают только пользователи, которым разрешено их раскрывать; все остальные получаютForbidden. Для публичных карточек используйте модель без маскированных полей.
Предпросмотр при наведении
.preview(screen, params) у ImageCard или ListItem со ссылкой показывает этот экран в панели рядом с карточкой: примерно через полсекунды наведения мыши или сразу, когда карточка получает фокус клавиатуры. Экран загружается только тогда, через GET /_zeyn/screen/... с теми же проверками доступа, что и у окна, и каждый предпросмотр хранится в течение жизни страницы (не более 24, очищается, когда nav.js открывает другой экран). Фреймы, объекты, медиаплееры и скрипты удаляются до показа панели, поэтому предпросмотр никогда не загружает iframe. Панель находится в верхнем слое, поэтому rail и сетки, обрезающие содержимое, её не режут; она открывается с той стороны, где больше места, переворачивается у краёв экрана, прокручивается внутри себя и никогда не прокручивает страницу. Она следует WCAG 1.4.13: указатель можно перевести на панель, Escape закрывает её, и она остаётся, пока указатель или фокус не уйдут. На сенсорных экранах и грубых указателях предпросмотра нет, а касание открывает ссылку; скрипт (preview.js, 2,5 КБ gzip) там не загружается никогда, а в остальных случаях загружается при первом наведении или фокусе на такую карточку. При уменьшенном движении панель появляется без короткой анимации увеличения.
Rail::of(search_products).card(|p: &Product| {
Card::image(p.image.clone(), p.name.clone())
.link(product, ProductParams { id: p.id })
.preview(product_summary, ProductParams { id: p.id })
})Rail
Rail это сетка карточек в одну строку, которая прокручивается вбок, с привязкой прокрутки и кнопками назад и вперёд, прокручивающими на ширину области просмотра (плавно, если только пользователь не просил уменьшить анимацию). Следующую порцию, по 12 штук, он загружает, когда пользователь прокручивает близко к концу. Карточки это ссылки в порядке обхода по Tab, и они прокручиваются в область видимости, когда получают фокус.
Полоса прокрутки скрыта; сенсорный экран, тачпад и Shift+колесо по-прежнему прокручивают, а .scrollbar(true) оставляет тонкую полосу. Rail можно перетаскивать мышью в сторону, удерживая кнопку. После сдвига более чем на 5 пикселей щелчок при отпускании отменяется, поэтому карточка под указателем не открывается, а rail останавливается на ближайшей карточке; меньший сдвиг считается обычным щелчком. Во время перетаскивания текст не выделяется, изображения не перетаскиваются наружу и предпросмотр карточки не открывается. На сенсорных экранах остается собственная прокрутка браузера. Кнопки «назад» и «вперед» есть всегда и отключаются на концах, потому что скрытая полоса прокрутки не позволяет прокручивать с клавиатуры. Без JavaScript кнопки скрыты, а rail прокручивается штатными средствами браузера.
Rail::of(catalog_products)
.title(t!("shop.new_arrivals"))
.card(|p: &Product| Card::image(p.image.clone(), p.name.clone()))
.sort_option(t!("shop.sort.newest"), Product::id, true)Поле поиска
Поле поиска с подсказками по шаблону W3C APG combobox со всплывающим списком (list box). По мере ввода оно ждёт instant миллисекунд и запрашивает у сервера подсказки: действие запускается с текстом в DataQuery::q, и возвращается текст подсказки для каждой строки (её первое немаскированное текстовое поле, если suggestion не говорит иначе). Enter или выбор подсказки открывает экран результатов с этим текстом, без перезагрузки страницы.
#[screen(route = "/shop/search", public)]
pub fn shop_search(p: SearchParams) -> Screen {
Screen::new(t!("shop.search.all"))
.body(SearchBox::of(catalog_products).value(p.q.clone()).results(shop_search))
.body(CardGrid::of(catalog_products).card(product_card).search(p.q).paging(Paging::pages(12)))
}
SearchBox::of(catalog_products)
.label(t!("shop.search.label"))
.instant(150)
.suggestion(|p: &Product| p.name.clone())
.results(shop_search)Клавиатура: стрелки вниз и вверх перемещают по подсказкам (стрелка вниз открывает их), Enter ищет по выделенной подсказке или по набранному тексту, Escape закрывает подсказки, а затем очищает поле. Живая область (live region) сообщает программам чтения с экрана, сколько подсказок.
Экран результатов принимает SearchParams (q, пусто, если экран открыт из меню). Сервер обрезает текст до MAX_SEARCH_QUERY (100) символов, решает, сколько подсказок отправить (8, если limit не говорит иначе, не более 20), проверяет доступ к экрану, защитные проверки (guards) feature и разрешение действия и учитывает каждый вызов подсказок в ограничении частоты записи (App::write_limit).
Свободный текст
DataQuery::q это свободный текст: строки, текстовые поля которых его содержат, без учёта регистра. Турецкие i с точкой и без точки (İ, ı) совпадают с i, поэтому "istanbul" находит "İSTANBUL". Поля с #[mask] никогда не участвуют в поиске, пустой текст совпадает со всеми строками, а текст длиннее 100 символов игнорируется. DataQuery::apply и слои данных SQL (LOWER(TRANSLATE(column, 'İı', 'ii')) LIKE ... со связанными параметрами) читают его одинаково.