Improve reinit/resize handling.

This commit is contained in:
Dermot Duffy
2022-07-23 10:54:07 -07:00
parent 325abdfdd8
commit 6010d7b1e1
5 changed files with 109 additions and 62 deletions
+68 -20
View File
@@ -15,12 +15,14 @@ import {
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { throttle } from 'lodash-es';
import carouselStyle from '../scss/carousel.scss';
import { TransitionEffect } from '../types';
import { dispatchFrigateCardEvent } from '../utils/basic.js';
export interface CarouselSelect {
index: number;
element: HTMLElement;
}
export type EmblaCarouselPlugins = CreatePluginType<
@@ -46,6 +48,18 @@ export class FrigateCardCarousel extends LitElement {
protected _carousel?: EmblaCarouselType;
// Whether the carousel is actively scrolling.
protected _scrolling = false;
// Whether to reinit the carousel when it settles.
protected _reInitOnSettle = false;
protected _carouselReInitInPlace = throttle(
this._carouselReInitInPlaceInternal.bind(this),
500,
{ trailing: true },
);
connectedCallback(): void {
super.connectedCallback();
@@ -104,20 +118,16 @@ export class FrigateCardCarousel extends LitElement {
/**
* Get the selected slide.
* @returns The slide index or undefined if the carousel is not loaded.
* @returns A CarouselSelect object (index & element).
*/
public carouselSelected(): number | undefined {
return this._carousel?.selectedScrollSnap();
}
/**
* Get the selected node.
* @returns The slide index or undefined if the carousel is not loaded.
*/
public carouselSelectedElement(): HTMLElement | null {
const selected = this._carousel?.selectedScrollSnap();
if (selected !== undefined) {
return this._carousel?.slideNodes()[selected] ?? null;
public getCarouselSelected(): CarouselSelect | null {
const index = this._carousel?.selectedScrollSnap();
const element = index !== undefined ? (this._carousel?.slideNodes()[index] ?? null) : null;
if (index !== undefined && element) {
return {
index: index,
element: element,
}
}
return null;
}
@@ -139,10 +149,38 @@ export class FrigateCardCarousel extends LitElement {
/**
* ReInit the carousel.
*/
public carouselReInit(): void {
protected _carouselReInit(options?: EmblaOptionsType): void {
window.requestAnimationFrame(() => {
// Safari appears to not loop the carousel unless the options are passed
// back in during re-initialization.
this._carousel?.reInit({ ...this.carouselOptions, ...options });
});
}
/**
* ReInit the carousel but stay on the current slide.
*/
protected _carouselReInitInPlaceInternal(): void {
const selected = this.getCarouselSelected();
// Safari appears to not loop the carousel unless the options are passed
// back in during re-initialization.
return this._carousel?.reInit(this.carouselOptions);
const options = {
...this.carouselOptions,
...(selected && { startIndex: selected.index }),
};
this._carouselReInit(options);
}
/**
* ReInit the carousel when it is safe to do so without disturbing the
* appearance (i.e. cutting off a scroll in progress).
*/
public carouselReInitWhenSafe(): void {
if (this._scrolling) {
this._reInitOnSettle = true;
} else {
this._carouselReInitInPlace();
}
}
/**
@@ -196,23 +234,33 @@ export class FrigateCardCarousel extends LitElement {
nodes,
{
axis: this.direction == 'horizontal' ? 'x' : 'y',
speed: 20,
...this.carouselOptions,
},
this.carouselPlugins,
);
this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init'));
this._carousel.on('select', () => {
const selected = this.carouselSelected();
if (selected !== undefined) {
dispatchFrigateCardEvent<CarouselSelect>(this, 'carousel:select', {
index: selected,
});
const selected = this.getCarouselSelected();
if (selected) {
dispatchFrigateCardEvent<CarouselSelect>(this, 'carousel:select', selected);
}
// Make sure every select causes a refresh to allow for re-paint of the
// next/previous controls.
this.requestUpdate();
});
this._carousel.on('scroll', () => {
this._scrolling = true;
});
this._carousel.on('settle', () => {
this._scrolling = false;
if (this._reInitOnSettle) {
this._reInitOnSettle = false;
this._carouselReInitInPlace();
}
});
}
}
+3 -4
View File
@@ -235,7 +235,7 @@ export class FrigateCardLiveCarousel extends LitElement {
this.view?.camera != oldView.camera
) {
const slide: number | undefined = this._cameraToSlide[this.view.camera];
if (slide !== undefined && slide !== frigateCardCarousel.carouselSelected()) {
if (slide !== undefined && slide !== frigateCardCarousel.getCarouselSelected()?.index) {
frigateCardCarousel.carouselScrollTo(slide);
}
}
@@ -310,8 +310,6 @@ export class FrigateCardLiveCarousel extends LitElement {
lazyUnloadCallback: (index, slide) =>
this._lazyloadOrUnloadSlide('unload', index, slide),
}),
// TODO: AutoMediaPlugin could be moved to MediaCarousel.
AutoMediaPlugin({
playerSelector: 'frigate-card-live-provider',
...(this.liveConfig?.auto_play && {
@@ -371,7 +369,8 @@ export class FrigateCardLiveCarousel extends LitElement {
protected _setViewHandler(): void {
const selectedCameraIndex = this._refMediaCarousel.value
?.frigateCardCarousel()
?.carouselSelected();
?.getCarouselSelected()
?.index;
if (selectedCameraIndex === undefined || !this.view || !this.cameras) {
return;
}
+31 -28
View File
@@ -18,7 +18,7 @@ import {
dispatchExistingMediaShowInfoAsEvent,
isValidMediaShowInfo,
} from '../utils/media-info.js';
import { EmblaCarouselPlugins, FrigateCardCarousel } from './carousel';
import { CarouselSelect, EmblaCarouselPlugins, FrigateCardCarousel } from './carousel';
import { AutoMediaType } from './embla-plugins/automedia.js';
import './next-prev-control.js';
import './carousel.js';
@@ -110,20 +110,25 @@ export class FrigateCardMediaCarousel extends LitElement {
protected _boundAutoPlayHandler = this.autoPlay.bind(this);
protected _boundAutoUnmuteHandler = this.autoUnmute.bind(this);
protected _boundAdaptiveHeightHandler = this._adaptiveHeightHandler.bind(this);
protected _boundAdaptiveHeightHandler = this._adaptHeightToMedia.bind(this);
protected _boundTitleHandler = this._titleHandler.bind(this);
// This carousel may be resized by Lovelace resizes, window resizes,
// fullscreen, etc. Always call the adaptive height handler when the size
// changes.
protected _resizeObserver: ResizeObserver;
protected _slideResizeObserver: ResizeObserver;
protected _intersectionObserver: IntersectionObserver;
protected _refCarousel: Ref<FrigateCardCarousel> = createRef();
constructor() {
super();
this._resizeObserver = new ResizeObserver(this._adaptiveHeightHandler.bind(this));
// Need to watch both changes in this element (e.g. caused by a window
// resize or fullscreen change) and changes in the selected slide itself
// (e.g. changing from a progress indicator to a loaded media).
this._resizeObserver = new ResizeObserver(this._reInitAndAdjustHeight.bind(this));
this._slideResizeObserver = new ResizeObserver(this._reInitAndAdjustHeight.bind(this));
this._intersectionObserver = new IntersectionObserver(
this._intersectionHandler.bind(this),
);
@@ -252,6 +257,14 @@ export class FrigateCardMediaCarousel extends LitElement {
super.disconnectedCallback();
}
/**
* ReInit the carousel and adapt the container height.
*/
protected _reInitAndAdjustHeight(): void {
this.frigateCardCarousel()?.carouselReInitWhenSafe();
this._adaptHeightToMedia();
}
/**
* Called when the carousel intersects with the viewport.
* @param entries The IntersectionObserverEntry entries (should be only 1).
@@ -267,24 +280,8 @@ export class FrigateCardMediaCarousel extends LitElement {
* - Example bug when this reinitialization is not performed:
* https://github.com/dermotduffy/frigate-hass-card/issues/651
*/
const reInit = (): void => {
// In some cases the carousel may need its height adjusted after the DOM is
// newly visible (e.g. a smaller camera live in preload mode, that becomes
// visible after switching from a larger camera snapshot).
this._adaptiveHeightHandler();
this.frigateCardCarousel()?.carouselReInit();
};
if (entries.some((entry) => entry.isIntersecting)) {
// For performance, run the reinit in idle cycles if the browser supports
// it, but only give it 400ms before running as it may otherwise be
// noticeable to the user.
if (window.requestIdleCallback !== undefined) {
window.requestIdleCallback(reInit, { timeout: 400 });
} else {
reInit();
}
this._reInitAndAdjustHeight();
}
}
@@ -293,14 +290,16 @@ export class FrigateCardMediaCarousel extends LitElement {
* have changed. This handler is not triggered from carousel events, as it's
* actually the media load/show that will change the dimensions, and that is
* async from carousel actions (e.g. lazy-loaded media).
*
* This component does not use the stock Embla auto-height plugin as it
* resizes the container on selection rather than media load.
*/
protected _adaptiveHeightHandler(): void {
protected _adaptHeightToMedia(): void {
const adaptCarouselHeight = (): void => {
const slide = this.frigateCardCarousel()?.carouselSelected();
if (slide !== undefined) {
const selected = this.frigateCardCarousel()?.getCarouselSelected();
if (selected) {
this.style.removeProperty('max-height');
const currentSlide = this.frigateCardCarousel()?.carouselSelectedElement();
const height = currentSlide?.getBoundingClientRect().height;
const height = selected.element.getBoundingClientRect().height;
if (height !== undefined && height > 0) {
this.style.maxHeight = `${height}px`;
}
@@ -321,7 +320,7 @@ export class FrigateCardMediaCarousel extends LitElement {
* Fire a media show event when a slide is selected.
*/
protected _dispatchMediaShowInfo(): void {
const slideIndex = this.frigateCardCarousel()?.carouselSelected();
const slideIndex = this.frigateCardCarousel()?.getCarouselSelected()?.index;
if (slideIndex !== undefined && slideIndex in this._mediaShowInfo) {
dispatchExistingMediaShowInfoAsEvent(this, this._mediaShowInfo[slideIndex]);
}
@@ -344,7 +343,7 @@ export class FrigateCardMediaCarousel extends LitElement {
// rejected upstream (empty 1x1 images will be rejected here).
if (mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) {
this._mediaShowInfo[slideIndex] = mediaShowInfo;
if (this.frigateCardCarousel()?.carouselSelected() === slideIndex) {
if (this.frigateCardCarousel()?.getCarouselSelected()?.index === slideIndex) {
dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo);
}
}
@@ -357,7 +356,11 @@ export class FrigateCardMediaCarousel extends LitElement {
.carouselPlugins=${this.carouselPlugins}
transitionEffect=${ifDefined(this.transitionEffect)}
@frigate-card:carousel:init=${this._dispatchMediaShowInfo.bind(this)}
@frigate-card:carousel:select=${this._dispatchMediaShowInfo.bind(this)}
@frigate-card:carousel:select=${(ev: CustomEvent<CarouselSelect>) => {
this._slideResizeObserver.disconnect();
this._slideResizeObserver.observe(ev.detail.element);
this._dispatchMediaShowInfo();
}}
@frigate-card:carousel:media-show=${this._storeMediaShowInfo.bind(this)}
>
<slot slot="previous" name="previous"></slot>
+1 -7
View File
@@ -80,13 +80,7 @@ export class FrigateCardThumbnailCarousel extends LitElement {
* Handle gallery resize.
*/
protected _resizeHandler(): void {
this._refCarousel.value?.carouselReInit();
// Reinit will cause the scroll position to reset, so re-scroll to the
// correct location.
if (this._selected !== null) {
this._refCarousel.value?.carouselScrollTo(this._selected);
}
this._refCarousel.value?.carouselReInitWhenSafe();
}
/**
+6 -3
View File
@@ -206,7 +206,10 @@ export class FrigateCardViewerCarousel extends LitElement {
this.view.childIndex != oldView.childIndex
) {
const slide = this._getSlideForChild(this.view.childIndex);
if (slide !== null && slide !== frigateCardCarousel.carouselSelected()) {
if (
slide !== null &&
slide !== frigateCardCarousel.getCarouselSelected()?.index
) {
// If the media target is the same as already loaded, but isn't of
// the selected slide, scroll to that slide.
frigateCardCarousel.carouselScrollTo(slide);
@@ -265,7 +268,7 @@ export class FrigateCardViewerCarousel extends LitElement {
if (!slide) {
slide = this._refMediaCarousel.value
?.frigateCardCarousel()
?.carouselSelectedElement();
?.getCarouselSelected()?.element;
}
return (
@@ -462,7 +465,7 @@ export class FrigateCardViewerCarousel extends LitElement {
// Update the childIndex in the view.
const selected = this._refMediaCarousel.value
.frigateCardCarousel()
?.carouselSelected();
?.getCarouselSelected()?.index;
if (selected !== undefined) {
const childIndex = this._slideToChild[selected];
if (childIndex !== undefined) {