Complete carousel refactor.
Reduces one layer of DOM nesting for simplication, uses the latest Embla version, unittests for everything.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { EmblaCarouselType, EmblaEventType } from 'embla-carousel';
|
||||
import { CreateOptionsType } from 'embla-carousel/components/Options';
|
||||
import { OptionsHandlerType } from 'embla-carousel/components/OptionsHandler';
|
||||
import { CreatePluginType, LoosePluginType } from 'embla-carousel/components/Plugins';
|
||||
import { LazyUnloadCondition } from '../../../../types';
|
||||
|
||||
declare module 'embla-carousel/components/Plugins' {
|
||||
interface EmblaPluginsType {
|
||||
lazyload?: AutoLazyLoadType;
|
||||
}
|
||||
}
|
||||
|
||||
type OptionsType = CreateOptionsType<{
|
||||
// Number of slides to lazyload left/right of selected (0 == only selected
|
||||
// slide).
|
||||
lazyLoadCount: number;
|
||||
lazyUnloadCondition?: LazyUnloadCondition;
|
||||
|
||||
lazyLoadCallback?: (index: number, slide: HTMLElement) => void;
|
||||
lazyUnloadCallback?: (index: number, slide: HTMLElement) => void;
|
||||
}>;
|
||||
type AutoLazyLoadOptionsType = Partial<OptionsType>;
|
||||
type AutoLazyLoadType = CreatePluginType<LoosePluginType, AutoLazyLoadOptionsType>;
|
||||
|
||||
const defaultOptions: OptionsType = {
|
||||
active: true,
|
||||
breakpoints: {},
|
||||
lazyLoadCount: 0,
|
||||
};
|
||||
|
||||
export function AutoLazyLoad(
|
||||
userOptions: AutoLazyLoadOptionsType = {},
|
||||
): AutoLazyLoadType {
|
||||
let options: OptionsType;
|
||||
let emblaApi: EmblaCarouselType;
|
||||
let slides: HTMLElement[];
|
||||
const lazyLoadedSlides: Set<number> = new Set();
|
||||
|
||||
const loadEvents: EmblaEventType[] = ['init', 'select'];
|
||||
const unloadEvents: EmblaEventType[] = ['select'];
|
||||
|
||||
function init(
|
||||
emblaApiInstance: EmblaCarouselType,
|
||||
optionsHandler: OptionsHandlerType,
|
||||
): void {
|
||||
const { mergeOptions, optionsAtMedia } = optionsHandler;
|
||||
const allOptions = mergeOptions(defaultOptions, userOptions);
|
||||
options = optionsAtMedia(allOptions);
|
||||
|
||||
emblaApi = emblaApiInstance;
|
||||
slides = emblaApi.slideNodes();
|
||||
|
||||
if (options.lazyLoadCallback) {
|
||||
loadEvents.forEach((evt) => emblaApi.on(evt, lazyLoadHandler));
|
||||
}
|
||||
if (
|
||||
options.lazyUnloadCallback &&
|
||||
options.lazyUnloadCondition &&
|
||||
['all', 'unselected'].includes(options.lazyUnloadCondition)
|
||||
) {
|
||||
unloadEvents.forEach((evt) => emblaApi.on(evt, lazyUnloadPreviousHandler));
|
||||
}
|
||||
document.addEventListener('visibilitychange', visibilityHandler);
|
||||
}
|
||||
|
||||
function destroy(): void {
|
||||
if (options.lazyLoadCallback) {
|
||||
loadEvents.forEach((evt) => emblaApi.off(evt, lazyLoadHandler));
|
||||
}
|
||||
if (options.lazyUnloadCallback) {
|
||||
unloadEvents.forEach((evt) => emblaApi.off(evt, lazyUnloadPreviousHandler));
|
||||
}
|
||||
document.removeEventListener('visibilitychange', visibilityHandler);
|
||||
}
|
||||
|
||||
function visibilityHandler(): void {
|
||||
if (
|
||||
document.visibilityState === 'hidden' &&
|
||||
options.lazyUnloadCondition &&
|
||||
['all', 'hidden'].includes(options.lazyUnloadCondition)
|
||||
) {
|
||||
lazyUnloadAllHandler();
|
||||
} else if (document.visibilityState === 'visible' && options.lazyLoadCallback) {
|
||||
lazyLoadHandler();
|
||||
}
|
||||
}
|
||||
|
||||
function hasLazyloaded(index: number): boolean {
|
||||
return lazyLoadedSlides.has(index);
|
||||
}
|
||||
|
||||
function lazyLoadHandler(): void {
|
||||
const lazyLoadCount = options.lazyLoadCount;
|
||||
const currentIndex = emblaApi.selectedScrollSnap();
|
||||
const slidesToLoad = new Set<number>();
|
||||
|
||||
// Lazily load 'count' slides on either side of the slides in view.
|
||||
for (let i = 1; i <= lazyLoadCount && currentIndex - i >= 0; i++) {
|
||||
slidesToLoad.add(currentIndex - i);
|
||||
}
|
||||
slidesToLoad.add(currentIndex);
|
||||
for (let i = 1; i <= lazyLoadCount && currentIndex + i < slides.length; i++) {
|
||||
slidesToLoad.add(currentIndex + i);
|
||||
}
|
||||
|
||||
slidesToLoad.forEach((index) => {
|
||||
if (!hasLazyloaded(index) && options.lazyLoadCallback) {
|
||||
lazyLoadedSlides.add(index);
|
||||
options.lazyLoadCallback(index, slides[index]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function lazyUnloadAllHandler(): void {
|
||||
lazyLoadedSlides.forEach((index) => {
|
||||
if (options.lazyUnloadCallback) {
|
||||
options.lazyUnloadCallback(index, slides[index]);
|
||||
lazyLoadedSlides.delete(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function lazyUnloadPreviousHandler(): void {
|
||||
const index = emblaApi.previousScrollSnap();
|
||||
|
||||
if (hasLazyloaded(index) && options.lazyUnloadCallback) {
|
||||
options.lazyUnloadCallback(index, slides[index]);
|
||||
lazyLoadedSlides.delete(index);
|
||||
}
|
||||
}
|
||||
|
||||
const self: AutoLazyLoadType = {
|
||||
name: 'autoLazyLoad',
|
||||
options: userOptions,
|
||||
init,
|
||||
destroy,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { EmblaCarouselType } from 'embla-carousel';
|
||||
import { CreateOptionsType } from 'embla-carousel/components/Options.js';
|
||||
import { OptionsHandlerType } from 'embla-carousel/components/OptionsHandler.js';
|
||||
import { CreatePluginType, LoosePluginType } from 'embla-carousel/components/Plugins.js';
|
||||
import {
|
||||
AutoMuteCondition,
|
||||
AutoPauseCondition,
|
||||
AutoPlayCondition,
|
||||
AutoUnmuteCondition,
|
||||
FrigateCardMediaPlayer,
|
||||
} from '../../../../types.js';
|
||||
|
||||
declare module 'embla-carousel/components/Plugins' {
|
||||
interface EmblaPluginsType {
|
||||
autoMediaActions?: AutoMediaActionsType;
|
||||
}
|
||||
}
|
||||
|
||||
type OptionsType = CreateOptionsType<{
|
||||
playerSelector?: string;
|
||||
|
||||
autoPlayCondition?: AutoPlayCondition;
|
||||
autoUnmuteCondition?: AutoUnmuteCondition;
|
||||
autoPauseCondition?: AutoPauseCondition;
|
||||
autoMuteCondition?: AutoMuteCondition;
|
||||
}>;
|
||||
export type AutoMediaActionsOptionsType = Partial<OptionsType>;
|
||||
|
||||
const defaultOptions: OptionsType = {
|
||||
active: true,
|
||||
breakpoints: {},
|
||||
};
|
||||
|
||||
export type AutoMediaActionsType = CreatePluginType<
|
||||
LoosePluginType,
|
||||
AutoMediaActionsOptionsType
|
||||
>;
|
||||
|
||||
export function AutoMediaActions(
|
||||
userOptions: AutoMediaActionsOptionsType = {},
|
||||
): AutoMediaActionsType {
|
||||
let options: OptionsType;
|
||||
let emblaApi: EmblaCarouselType;
|
||||
let slides: HTMLElement[];
|
||||
let hadInitialIntersectionCall: boolean | null = false;
|
||||
|
||||
const intersectionObserver: IntersectionObserver = new IntersectionObserver(
|
||||
intersectionHandler,
|
||||
);
|
||||
|
||||
function init(
|
||||
emblaApiInstance: EmblaCarouselType,
|
||||
optionsHandler: OptionsHandlerType,
|
||||
): void {
|
||||
emblaApi = emblaApiInstance;
|
||||
|
||||
const { mergeOptions, optionsAtMedia } = optionsHandler;
|
||||
options = optionsAtMedia(mergeOptions(defaultOptions, userOptions));
|
||||
|
||||
slides = emblaApi.slideNodes();
|
||||
|
||||
if (
|
||||
options.autoPlayCondition &&
|
||||
['all', 'selected'].includes(options.autoPlayCondition)
|
||||
) {
|
||||
// Auto play when the media loads not necessarily when the slide is
|
||||
// selected (to allow for lazyloading).
|
||||
emblaApi.containerNode().addEventListener('frigate-card:media:loaded', play);
|
||||
}
|
||||
|
||||
if (
|
||||
options.autoUnmuteCondition &&
|
||||
['all', 'selected'].includes(options.autoUnmuteCondition)
|
||||
) {
|
||||
// Auto unmute when the media loads not necessarily when the slide is
|
||||
// selected (to allow for lazyloading).
|
||||
emblaApi.containerNode().addEventListener('frigate-card:media:loaded', unmute);
|
||||
}
|
||||
|
||||
if (
|
||||
options.autoPauseCondition &&
|
||||
['all', 'unselected'].includes(options.autoPauseCondition)
|
||||
) {
|
||||
emblaApi.on('select', pausePrevious);
|
||||
}
|
||||
|
||||
if (
|
||||
options.autoMuteCondition &&
|
||||
['all', 'unselected'].includes(options.autoMuteCondition)
|
||||
) {
|
||||
emblaApi.on('select', mutePrevious);
|
||||
}
|
||||
|
||||
emblaApi.on('destroy', pause);
|
||||
emblaApi.on('destroy', mute);
|
||||
|
||||
document.addEventListener('visibilitychange', visibilityHandler);
|
||||
intersectionObserver.observe(emblaApi.containerNode());
|
||||
}
|
||||
|
||||
function destroy(): void {
|
||||
if (
|
||||
options.autoPlayCondition &&
|
||||
['all', 'selected'].includes(options.autoPlayCondition)
|
||||
) {
|
||||
emblaApi.containerNode().removeEventListener('frigate-card:media:loaded', play);
|
||||
}
|
||||
|
||||
if (
|
||||
options.autoUnmuteCondition &&
|
||||
['all', 'selected'].includes(options.autoUnmuteCondition)
|
||||
) {
|
||||
emblaApi.containerNode().removeEventListener('frigate-card:media:loaded', unmute);
|
||||
}
|
||||
|
||||
if (
|
||||
options.autoPauseCondition &&
|
||||
['all', 'unselected'].includes(options.autoPauseCondition)
|
||||
) {
|
||||
emblaApi.off('select', pausePrevious);
|
||||
}
|
||||
|
||||
if (
|
||||
options.autoMuteCondition &&
|
||||
['all', 'unselected'].includes(options.autoMuteCondition)
|
||||
) {
|
||||
emblaApi.off('select', mutePrevious);
|
||||
}
|
||||
|
||||
emblaApi.off('destroy', pause);
|
||||
emblaApi.off('destroy', mute);
|
||||
|
||||
document.removeEventListener('visibilitychange', visibilityHandler);
|
||||
intersectionObserver.disconnect();
|
||||
}
|
||||
|
||||
function actOnVisibilityChange(visible: boolean): void {
|
||||
if (visible) {
|
||||
if (
|
||||
options.autoPlayCondition &&
|
||||
['all', 'visible'].includes(options.autoPlayCondition)
|
||||
) {
|
||||
play();
|
||||
}
|
||||
if (
|
||||
options.autoUnmuteCondition &&
|
||||
['all', 'visible'].includes(options.autoUnmuteCondition)
|
||||
) {
|
||||
unmute();
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
options.autoPauseCondition &&
|
||||
['all', 'hidden'].includes(options.autoPauseCondition)
|
||||
) {
|
||||
pauseAll();
|
||||
}
|
||||
if (
|
||||
options.autoMuteCondition &&
|
||||
['all', 'hidden'].includes(options.autoMuteCondition)
|
||||
) {
|
||||
muteAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function visibilityHandler(): void {
|
||||
actOnVisibilityChange(document.visibilityState === 'visible');
|
||||
}
|
||||
|
||||
function intersectionHandler(entries: IntersectionObserverEntry[]): void {
|
||||
if (!hadInitialIntersectionCall) {
|
||||
hadInitialIntersectionCall = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the live view is preloaded (i.e. in the background) we may need to
|
||||
// take media actions, e.g. muting a live stream that is now running in the
|
||||
// background.
|
||||
actOnVisibilityChange(entries.some((entry) => entry.isIntersecting));
|
||||
}
|
||||
|
||||
function getPlayer(slide: HTMLElement | undefined): FrigateCardMediaPlayer | null {
|
||||
return options.playerSelector
|
||||
? (slide?.querySelector(options.playerSelector) as FrigateCardMediaPlayer | null)
|
||||
: null;
|
||||
}
|
||||
|
||||
function play(): void {
|
||||
getPlayer(slides[emblaApi.selectedScrollSnap()])?.play();
|
||||
}
|
||||
|
||||
function pause(): void {
|
||||
getPlayer(slides[emblaApi.selectedScrollSnap()])?.pause();
|
||||
}
|
||||
|
||||
function pausePrevious(): void {
|
||||
getPlayer(slides[emblaApi.previousScrollSnap()])?.pause();
|
||||
}
|
||||
|
||||
function pauseAll(): void {
|
||||
for (const slide of slides) {
|
||||
getPlayer(slide)?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
function unmute(): void {
|
||||
getPlayer(slides[emblaApi.selectedScrollSnap()])?.unmute();
|
||||
}
|
||||
|
||||
function mute(): void {
|
||||
getPlayer(slides[emblaApi.selectedScrollSnap()])?.mute();
|
||||
}
|
||||
|
||||
function mutePrevious(): void {
|
||||
getPlayer(slides[emblaApi.previousScrollSnap()])?.mute();
|
||||
}
|
||||
|
||||
function muteAll(): void {
|
||||
for (const slide of slides) {
|
||||
getPlayer(slide)?.mute();
|
||||
}
|
||||
}
|
||||
|
||||
const self: AutoMediaActionsType = {
|
||||
name: 'autoMediaActions',
|
||||
options: userOptions,
|
||||
init,
|
||||
destroy,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { EmblaCarouselType } from 'embla-carousel';
|
||||
import { CreatePluginType, LoosePluginType } from 'embla-carousel/components/Plugins';
|
||||
import { MediaLoadedInfo } from '../../../../types';
|
||||
import {
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
FrigateMediaLoadedEventTarget,
|
||||
} from '../../../media-info';
|
||||
import { LooseOptionsType } from 'embla-carousel/components/Options';
|
||||
|
||||
declare module 'embla-carousel/components/Plugins' {
|
||||
interface EmblaPluginsType {
|
||||
autoMediaLoadedInfo?: AutoMediaLoadedInfoType;
|
||||
}
|
||||
}
|
||||
|
||||
type AutoMediaLoadedInfoType = CreatePluginType<LoosePluginType, LooseOptionsType>;
|
||||
|
||||
function AutoMediaLoadedInfo(): AutoMediaLoadedInfoType {
|
||||
let emblaApi: EmblaCarouselType;
|
||||
let slides: (HTMLElement & FrigateMediaLoadedEventTarget)[] = [];
|
||||
const mediaLoadedInfo: MediaLoadedInfo[] = [];
|
||||
|
||||
function init(emblaApiInstance: EmblaCarouselType): void {
|
||||
emblaApi = emblaApiInstance;
|
||||
slides = emblaApi.slideNodes();
|
||||
|
||||
for (const slide of slides) {
|
||||
slide.addEventListener('frigate-card:media:loaded', mediaLoadedInfoHandler);
|
||||
slide.addEventListener('frigate-card:media:unloaded', mediaUnloadedInfoHandler);
|
||||
}
|
||||
|
||||
emblaApi.on('init', slideSelectHandler);
|
||||
emblaApi.on('select', slideSelectHandler);
|
||||
}
|
||||
|
||||
function destroy(): void {
|
||||
for (const slide of slides) {
|
||||
slide.removeEventListener('frigate-card:media:loaded', mediaLoadedInfoHandler);
|
||||
slide.removeEventListener('frigate-card:media:unloaded', mediaUnloadedInfoHandler);
|
||||
}
|
||||
|
||||
emblaApi.off('init', slideSelectHandler);
|
||||
emblaApi.off('select', slideSelectHandler);
|
||||
}
|
||||
|
||||
function mediaLoadedInfoHandler(ev: CustomEvent<MediaLoadedInfo>): void {
|
||||
const eventPath = ev.composedPath();
|
||||
|
||||
for (const [index, slide] of slides.entries()) {
|
||||
if (eventPath.includes(slide)) {
|
||||
mediaLoadedInfo[index] = ev.detail;
|
||||
if (index !== emblaApi.selectedScrollSnap()) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mediaUnloadedInfoHandler(ev: CustomEvent): void {
|
||||
const eventPath = ev.composedPath();
|
||||
|
||||
for (const [index, slide] of slides.entries()) {
|
||||
if (eventPath.includes(slide)) {
|
||||
delete mediaLoadedInfo[index];
|
||||
if (index !== emblaApi.selectedScrollSnap()) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function slideSelectHandler(): void {
|
||||
const index = emblaApi.selectedScrollSnap();
|
||||
const savedMediaLoadedInfo: MediaLoadedInfo | undefined = mediaLoadedInfo[index];
|
||||
if (savedMediaLoadedInfo) {
|
||||
dispatchExistingMediaLoadedInfoAsEvent(
|
||||
emblaApi.containerNode(),
|
||||
savedMediaLoadedInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const self: AutoMediaLoadedInfoType = {
|
||||
name: 'autoMediaLoadedInfo',
|
||||
options: {},
|
||||
init,
|
||||
destroy,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
export default AutoMediaLoadedInfo;
|
||||
@@ -0,0 +1,143 @@
|
||||
import { EmblaCarouselType } from 'embla-carousel';
|
||||
import { LooseOptionsType } from 'embla-carousel/components/Options';
|
||||
import { CreatePluginType, LoosePluginType } from 'embla-carousel/components/Plugins';
|
||||
import { EmblaReInitController } from '../../reinit-controller';
|
||||
|
||||
declare module 'embla-carousel/components/Plugins' {
|
||||
interface EmblaPluginsType {
|
||||
AutoSize?: AutoSizeType;
|
||||
}
|
||||
}
|
||||
|
||||
type AutoSizeType = CreatePluginType<LoosePluginType, LooseOptionsType>;
|
||||
interface SlideDimensions {
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This plugin offers the following functionality:
|
||||
* - Auto-height: Automatically resize the container to fit the largest slide on
|
||||
* view. Unlike the stock `auto-height` plugin, this version will use active
|
||||
* DOM sizing vs the internal engine sizes to account for pre-reinit resize
|
||||
* detection.
|
||||
* - Resize and intersection re-initializing: Re-initialize the carousel on
|
||||
* slide or container resizes, or container intersection changes.
|
||||
*/
|
||||
|
||||
function AutoSize(): AutoSizeType {
|
||||
let emblaApi: EmblaCarouselType;
|
||||
let reInitController: EmblaReInitController | null = null;
|
||||
|
||||
let previousContainerIntersecting: boolean | null = null;
|
||||
const previousDimensions: Map<Element, SlideDimensions> = new Map();
|
||||
|
||||
const resizeObserver: ResizeObserver = new ResizeObserver(resizeHandler);
|
||||
const intersectionObserver: IntersectionObserver = new IntersectionObserver(
|
||||
intersectionHandler,
|
||||
);
|
||||
|
||||
function init(emblaApiInstance: EmblaCarouselType): void {
|
||||
emblaApi = emblaApiInstance;
|
||||
reInitController = new EmblaReInitController(emblaApi);
|
||||
|
||||
intersectionObserver.observe(emblaApi.containerNode());
|
||||
resizeObserver.observe(emblaApi.containerNode());
|
||||
for (const slide of emblaApi.slideNodes()) {
|
||||
resizeObserver.observe(slide);
|
||||
}
|
||||
|
||||
emblaApi.on('settle', setContainerHeight);
|
||||
}
|
||||
|
||||
function destroy(): void {
|
||||
intersectionObserver.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
reInitController?.destroy();
|
||||
|
||||
emblaApi.off('settle', setContainerHeight);
|
||||
}
|
||||
|
||||
function intersectionHandler(entries: IntersectionObserverEntry[]): void {
|
||||
/**
|
||||
* - If the DOM that contains this carousel changes such that it causes
|
||||
* slides to entirely appear/disappear (e.g. `display: none` or hidden),
|
||||
* then the displayed slide sizes will significantly change and the
|
||||
* carousel will need to be reinitialized. Without this, odd bugs may
|
||||
* occur for some users in some circumstances causing the carousel to
|
||||
* appear 'stuck'.
|
||||
* - Example bug when this reinitialization is not performed:
|
||||
* https://github.com/dermotduffy/frigate-hass-card/issues/651
|
||||
*/
|
||||
const isContainerIntersectingNow = entries.some((entry) => entry.isIntersecting);
|
||||
|
||||
if (isContainerIntersectingNow !== previousContainerIntersecting) {
|
||||
// Don't reinitialize on first call (intersectionHandler is always called
|
||||
// on initial observation).
|
||||
const callReInit = previousContainerIntersecting !== null;
|
||||
previousContainerIntersecting = isContainerIntersectingNow;
|
||||
if (callReInit) {
|
||||
reInitController?.reinit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resizeHandler(entries: ResizeObserverEntry[]): void {
|
||||
let callReInit = false;
|
||||
|
||||
for (const entry of entries) {
|
||||
const newDimensions: SlideDimensions = {
|
||||
height: entry.contentRect.height,
|
||||
width: entry.contentRect.width,
|
||||
};
|
||||
|
||||
const oldDimensions = previousDimensions.get(entry.target);
|
||||
if (
|
||||
newDimensions.width &&
|
||||
newDimensions.height &&
|
||||
(oldDimensions?.height !== newDimensions.height ||
|
||||
oldDimensions?.width !== newDimensions.width)
|
||||
) {
|
||||
previousDimensions.set(entry.target, newDimensions);
|
||||
callReInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (callReInit) {
|
||||
reInitController?.reinit();
|
||||
}
|
||||
}
|
||||
|
||||
function setContainerHeight(): void {
|
||||
const {
|
||||
slideRegistry,
|
||||
options: { axis },
|
||||
} = emblaApi.internalEngine();
|
||||
|
||||
if (axis === 'y') {
|
||||
return;
|
||||
}
|
||||
|
||||
emblaApi.containerNode().style.removeProperty('max-height');
|
||||
|
||||
const selectedIndexes = slideRegistry[emblaApi.selectedScrollSnap()];
|
||||
const slides = emblaApi.slideNodes();
|
||||
const highest = Math.max(
|
||||
...selectedIndexes.map((i) => slides[i].getBoundingClientRect().height),
|
||||
);
|
||||
|
||||
if (!isNaN(highest) && highest > 0) {
|
||||
emblaApi.containerNode().style.maxHeight = `${highest}px`;
|
||||
}
|
||||
}
|
||||
|
||||
const self: AutoSizeType = {
|
||||
name: 'autoSize',
|
||||
options: {},
|
||||
init,
|
||||
destroy,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
export default AutoSize;
|
||||
Reference in New Issue
Block a user