UNPKG

@empathyco/x-components

Version:
180 lines (177 loc) 7.74 kB
import { defineComponent, ref, computed, provide, onMounted, getCurrentInstance, onBeforeUnmount, watch } from 'vue'; import { DISABLE_ANIMATIONS_KEY } from '../../../components/decorators/injection.consts.js'; import { useState } from '../../../composables/use-state.js'; import { useXBus } from '../../../composables/use-x-bus.js'; import { scrollXModule } from '../x-module.js'; import { ScrollObserverKey } from './scroll.const.js'; /** * Extends the scroll making it able to sync the first visible element, and allowing * the children position to be restored. * * Each child element that wants to have this support must be wrapped in a {@link MainScrollItem} * component. * * @public */ var _sfc_main = defineComponent({ name: 'MainScroll', xModule: scrollXModule.name, props: { /** * If `true`, sets this scroll instance to the main of the application. Being the main * scroll implies that features like restoring the scroll when the query changes, or storing * the scroll position in the URL will be enabled for this container. */ useWindow: { type: Boolean, default: false, }, /** * Timeout in milliseconds to abort trying to restore the scroll position to the target * element. */ restoreScrollTimeoutMs: { type: Number, default: 5000, }, /** Intersection percentage to consider an element visible. */ threshold: { type: Number, default: 0.3, }, /** Adjusts the size of the scroll container bounds. */ margin: { type: String, default: '0px', }, }, setup(props, { slots }) { const xBus = useXBus(); /** The reference to the root element of the component. */ const rootRef = ref(); /** The elements that are currently considered visible. */ const intersectingElements = ref([]); /** Intersection observer to determine visibility of the elements. */ const intersectionObserver = ref(null); /** * Pending identifier scroll position to restore. If it matches the {@link MainScrollItem} item * `id` property, this component should be scrolled into view. */ const { pendingScrollTo } = useState('scroll'); /** Disables the animations. */ const disableAnimations = computed(() => !!pendingScrollTo.value); provide(DISABLE_ANIMATIONS_KEY, disableAnimations); /** * Removes an element from the {@link MainScroll.intersectingElements} list. * * @param element - The element to remove from the visible elements. */ const removeVisibleElement = (element) => { const index = intersectingElements.value.indexOf(element); if (index !== -1) { intersectingElements.value.splice(index, 1); } }; /** * Creates an `IntersectionObserver` to detect the first visible elements. Children of this * component should register themselves if they want to be observed. * * @returns The intersection observer. */ const visibleElementsObserver = computed(() => { const observer = intersectionObserver.value; return observer ? { observe: observer.observe.bind(observer), unobserve: element => { removeVisibleElement(element); observer.unobserve(element); }, } : null; }); provide(ScrollObserverKey, visibleElementsObserver); /** * Updates the visible elements given a list of intersection observer entries. * * @param entries - The entries from whom update the visibility. */ const updateVisibleElements = (entries) => { entries.forEach(entry => { const target = entry.target; if (entry.isIntersecting) { intersectingElements.value.push(target); } else { removeVisibleElement(target); } }); }; /** Stores the root element and initialise the observer after mounting the component. */ onMounted(() => { rootRef.value = getCurrentInstance()?.proxy?.$el; if (rootRef.value) { intersectionObserver.value = new IntersectionObserver(updateVisibleElements, { root: props.useWindow ? document : rootRef.value, threshold: props.threshold, rootMargin: props.margin, }); } }); /** Disconnects the intersection observer. */ onBeforeUnmount(() => { intersectionObserver.value?.disconnect(); xBus.emit('UserScrolledToElement', ''); }); /** Disconnects the previous observer. */ watch(intersectionObserver, (_new, old) => old?.disconnect()); /** Stores the identifier of the timeout that will consider the scroll failed to restore. */ let restoreScrollFailTimeoutId; /** * If there is a pending scroll, starts a countdown to stop trying to restore the scroll. * * @param pendingScrollTo - The position the scroll should be restored to. */ watch(pendingScrollTo, () => { // TODO Move this logic to the wiring. A cancelable delay operator is needed clearTimeout(restoreScrollFailTimeoutId); if (pendingScrollTo.value) { restoreScrollFailTimeoutId = window.setTimeout(() => { xBus.emit('ScrollRestoreFailed'); }, props.restoreScrollTimeoutMs); } }); /** * The first visible element contained in this component. * * @returns The first visible element in this component. */ const firstVisibleElement = computed(() => { if (intersectingElements.value.length === 0) { return ''; } const firstVisibleElement = intersectingElements.value.reduce((firstVisibleElement, anotherElement) => { // FIXME: This algorithm only takes into account LTR layouts const firstVisibleElementBounds = firstVisibleElement.getBoundingClientRect(); const anotherElementBounds = anotherElement.getBoundingClientRect(); return anotherElementBounds.left <= firstVisibleElementBounds.left && anotherElementBounds.top <= firstVisibleElementBounds.top ? anotherElement : firstVisibleElement; }); return firstVisibleElement === rootRef.value?.querySelector('[data-scroll]') ? '' : firstVisibleElement.dataset.scroll; }); watch(firstVisibleElement, () => xBus.emit('UserScrolledToElement', firstVisibleElement.value), { immediate: true }); /* * Obtains the vNodes array of the default slot and renders only the first one. * It avoids to render a `Fragment` with the vNodes in Vue3 and the same behaviour in Vue2 * because Vue2 only allows a single root node. Then, `getCurrentInstance()?.proxy?.$el` to * retrieve the HTML element in both versions. */ return () => slots.default?.()[0] ?? ''; }, }); export { _sfc_main as default }; //# sourceMappingURL=main-scroll.vue.js.map