Контроллер reveal-эффектов
createRevealController(options?) — сканирует DOM на элементы, подходящие под селектор, и переключает класс/атрибут на каждом по мере появления во viewport, без вызова useElementVisibility()/ observe() на каждый элемент. Построен целиком поверх пулящегося обсервера createVisibilityEngine — MutationObserver (по умолчанию следит за document.body) подхватывает элементы, подходящие под selector, появившиеся позже (например, блок v-for, отрендеренный после асинхронного запроса), и снимает наблюдение с элементов, удалённых из DOM.
function createRevealController(options?: RevealControllerOptions): RevealControllerimport { createRevealController } from '@macrulez/inview-core'
const controller = createRevealController({
selector: '[data-reveal], .reveal', // значение по умолчанию
stagger: { step: 70, max: 4 },
})
// позже, например при демонтаже приложения
controller.destroy().reveal {
opacity: 0;
transition: opacity 0.4s ease-out;
transition-delay: var(--reveal-delay, 0ms);
}
.reveal.in {
opacity: 1;
}Опции
RevealControllerOptions
| Поле | Тип | По умолчанию |
|---|---|---|
selector | string | '[data-reveal], .reveal' |
activeClass | string | null | 'in' — null отключает переключение класса |
activeAttribute | string | null | null — булев data-атрибут, выставляется вместе с activeClass |
once | boolean | true — в отличие от false у useElementVisibility; reveal-эффектам это почти всегда нужно |
threshold | number | number[] | 0 |
rootMargin | string | '0px' |
root | Element | null | null |
stagger | RevealStaggerOptions | false | {} — false отключает |
watchMutations | boolean | true |
watchRoot | Element | document.body |
onEnter / onLeave | (el: Element, info: IntersectionInfo) => void | — та же форма info, что в Движке видимости, первым аргументом — сам элемент, потому что один контроллер следит за многими |
pool | ObserverPool | общий пул по умолчанию |
RevealStaggerOptions
Расширяет StaggerDelayOptions (step, max, unit, mode) двумя дополнительными полями:
| Поле | Тип | По умолчанию |
|---|---|---|
cssVar | string | null | '--reveal-delay' — null полностью отключает запись CSS-переменной |
applyInlineDelay | boolean | true |
По умолчанию вычисленная задержка записывается сразу двумя способами: как CSS-переменная --reveal-delay через bindCSSVar, и как инлайн-стиль transition-delay прямо на элементе. Именно инлайн-стиль реально побеждает в каскаде — правило вроде .card { transition: ... }, объявленное позже (например, transition при hover), иначе молча перебило бы transition-delay: var(--reveal-delay) без !important. CSS-переменная при этом продолжает записываться — она, в отличие от инлайн-стиля, наследуется, что нужно для селектора потомка вроде .reveal > .icon { transition-delay: var(--reveal-delay) }. Передайте applyInlineDelay: false, чтобы записывать только CSS-переменную, или cssVar: null, чтобы выставлять только инлайн-стиль.
Задержка стаггера полностью пропускается — без CSS-переменной, без инлайн-стиля, без слушателя очистки — когда prefersReducedMotion() возвращает true, так же как при явном stagger: false. Само переключение класса/атрибута при этом всё равно происходит как обычно — вопрос движения касается только таймингов стаггера.
Переопределения на уровне элемента
Любую опцию контроллера можно переопределить на конкретном элементе через соответствующий data-reveal-* атрибут — значение контроллера применяется только там, где элемент не сказал иначе.
| Атрибут | Переопределяет |
|---|---|
data-reveal-once="false" | once |
data-reveal-threshold="0,0.5" | threshold — через запятую для массива |
data-reveal-root-margin="-10%" | rootMargin |
data-reveal-class="visible" | activeClass, только для этого элемента |
data-reveal-attribute="active" | activeAttribute, только для этого элемента |
data-reveal-delay="140" | явная задержка стаггера в мс, полностью пропуская вычисленную |
data-reveal-group="grid-a" | индекс стаггера считается внутри названной группы, а не глобально |
<div class="reveal" data-reveal-once="false" data-reveal-threshold="0.4">
Переключается туда-обратно, вместо того чтобы остановиться после первого показа.
</div>Возвращаемое значение
refresh(selector?)
(selector?: string) => void
Повторно сканирует на ещё не отслеживаемые элементы. Редко нужен — watchMutations (включён по умолчанию) уже делает это автоматически — полезен сразу после синхронного изменения DOM, если watchMutations выключен. Передайте селектор, чтобы отсканировать этим конкретным вызовом что-то отличное от собственного selector контроллера (например, более узкий, ограниченный только что отрендеренным поддеревом), не меняя при этом, что ищет сам watchMutations дальше.
observe(el)
(el: Element) => void
Начинает наблюдать за el напрямую, минуя скан по selector — для элемента, на который уже есть ссылка и для которого не хочется полагаться на совпадение с селектором. Не делает ничего, если el уже наблюдается.
unobserve(el)
(el: Element) => void
Прекращает наблюдение за el, так же как если бы он был удалён из DOM.
destroy()
() => void
Отключает MutationObserver и снимает наблюдение со всех отслеживаемых на данный момент элементов.
Пример: reveal-проход по всей странице
Тот самый сценарий, для которого всё это существует — около 100 элементов с классом на нескольких динамически рендерящихся списках, часть из которых появляется после асинхронного запроса:
import { createRevealController } from '@macrulez/inview-core'
const controller = createRevealController({
stagger: { step: 60, max: 4 },
})<template>
<!-- отрендерено до завершения запроса — MutationObserver контроллера
сам подхватит эти элементы, как только они появятся -->
<div v-for="item in items" :key="item.id" class="reveal">
{{ item.title }}
</div>
</template>Никакого компонента-обёртки на каждый элемент списка, никакого вызова useElementVisibility() на каждый элемент — контроллер сам находит .reveal-элементы, сейчас и позже.
Обёртка жизненного цикла: useRevealController
И у Vue, и у React есть тонкая обёртка жизненного цикла, убирающая ручной шаблонный код onMounted/useEffect + destroy(), который иначе нужен для контроллера на всю страницу в каждом месте вызова — создаёт контроллер при монтировании, уничтожает автоматически при размонтировании и возвращает ту же форму refresh/observe/unobserve/destroy:
function useRevealController(options?: RevealControllerOptions): RevealController<script setup lang="ts">
import { useRevealController } from '@macrulez/inview-vue'
useRevealController({ stagger: { step: 70, max: 4 } })
</script>import { useRevealController } from '@macrulez/inview-react'
function App() {
useRevealController({ stagger: { step: 70, max: 4 } })
return <Page />
}options захватываются один раз, при монтировании, и используются на весь срок жизни контроллера — та же одноразовая модель опций, что и у самого createRevealController, а не что-то, реагирующее на изменение объекта options при каждом ре-рендере/повторном вызове.
Когда лучше v-reveal/<InView>, а не это
v-reveal и <InView> (Vue) дают контроль на уровне отдельного элемента — свой onEnter для каждого, или реактивный isVisible прямо в шаблоне. createRevealController — для разметки, которую не хочется размечать поэлементно вообще: один вызов покрывает всю страницу (или контейнер), включая элементы, которых ещё нет в DOM. Это также единственный из трёх вариантов, который работает одинаково вообще без фреймворка — про React смотрите React-хуки (там же useRevealController выше, либо прямой вызов createRevealController).