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
+67 -19
View File
@@ -15,12 +15,14 @@ import {
} from 'lit'; } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js'; import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { throttle } from 'lodash-es';
import carouselStyle from '../scss/carousel.scss'; import carouselStyle from '../scss/carousel.scss';
import { TransitionEffect } from '../types'; import { TransitionEffect } from '../types';
import { dispatchFrigateCardEvent } from '../utils/basic.js'; import { dispatchFrigateCardEvent } from '../utils/basic.js';
export interface CarouselSelect { export interface CarouselSelect {
index: number; index: number;
element: HTMLElement;
} }
export type EmblaCarouselPlugins = CreatePluginType< export type EmblaCarouselPlugins = CreatePluginType<
@@ -46,6 +48,18 @@ export class FrigateCardCarousel extends LitElement {
protected _carousel?: EmblaCarouselType; 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 { connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
@@ -104,20 +118,16 @@ export class FrigateCardCarousel extends LitElement {
/** /**
* Get the selected slide. * 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 { public getCarouselSelected(): CarouselSelect | null {
return this._carousel?.selectedScrollSnap(); const index = this._carousel?.selectedScrollSnap();
const element = index !== undefined ? (this._carousel?.slideNodes()[index] ?? null) : null;
if (index !== undefined && element) {
return {
index: index,
element: element,
} }
/**
* 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;
} }
return null; return null;
} }
@@ -139,10 +149,38 @@ export class FrigateCardCarousel extends LitElement {
/** /**
* ReInit the carousel. * 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 // Safari appears to not loop the carousel unless the options are passed
// back in during re-initialization. // back in during re-initialization.
return this._carousel?.reInit(this.carouselOptions); 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.
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, nodes,
{ {
axis: this.direction == 'horizontal' ? 'x' : 'y', axis: this.direction == 'horizontal' ? 'x' : 'y',
speed: 20,
...this.carouselOptions, ...this.carouselOptions,
}, },
this.carouselPlugins, this.carouselPlugins,
); );
this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init')); this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init'));
this._carousel.on('select', () => { this._carousel.on('select', () => {
const selected = this.carouselSelected(); const selected = this.getCarouselSelected();
if (selected !== undefined) { if (selected) {
dispatchFrigateCardEvent<CarouselSelect>(this, 'carousel:select', { dispatchFrigateCardEvent<CarouselSelect>(this, 'carousel:select', selected);
index: selected,
});
} }
// Make sure every select causes a refresh to allow for re-paint of the // Make sure every select causes a refresh to allow for re-paint of the
// next/previous controls. // next/previous controls.
this.requestUpdate(); 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 this.view?.camera != oldView.camera
) { ) {
const slide: number | undefined = this._cameraToSlide[this.view.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); frigateCardCarousel.carouselScrollTo(slide);
} }
} }
@@ -310,8 +310,6 @@ export class FrigateCardLiveCarousel extends LitElement {
lazyUnloadCallback: (index, slide) => lazyUnloadCallback: (index, slide) =>
this._lazyloadOrUnloadSlide('unload', index, slide), this._lazyloadOrUnloadSlide('unload', index, slide),
}), }),
// TODO: AutoMediaPlugin could be moved to MediaCarousel.
AutoMediaPlugin({ AutoMediaPlugin({
playerSelector: 'frigate-card-live-provider', playerSelector: 'frigate-card-live-provider',
...(this.liveConfig?.auto_play && { ...(this.liveConfig?.auto_play && {
@@ -371,7 +369,8 @@ export class FrigateCardLiveCarousel extends LitElement {
protected _setViewHandler(): void { protected _setViewHandler(): void {
const selectedCameraIndex = this._refMediaCarousel.value const selectedCameraIndex = this._refMediaCarousel.value
?.frigateCardCarousel() ?.frigateCardCarousel()
?.carouselSelected(); ?.getCarouselSelected()
?.index;
if (selectedCameraIndex === undefined || !this.view || !this.cameras) { if (selectedCameraIndex === undefined || !this.view || !this.cameras) {
return; return;
} }
+31 -28
View File
@@ -18,7 +18,7 @@ import {
dispatchExistingMediaShowInfoAsEvent, dispatchExistingMediaShowInfoAsEvent,
isValidMediaShowInfo, isValidMediaShowInfo,
} from '../utils/media-info.js'; } from '../utils/media-info.js';
import { EmblaCarouselPlugins, FrigateCardCarousel } from './carousel'; import { CarouselSelect, EmblaCarouselPlugins, FrigateCardCarousel } from './carousel';
import { AutoMediaType } from './embla-plugins/automedia.js'; import { AutoMediaType } from './embla-plugins/automedia.js';
import './next-prev-control.js'; import './next-prev-control.js';
import './carousel.js'; import './carousel.js';
@@ -110,20 +110,25 @@ export class FrigateCardMediaCarousel extends LitElement {
protected _boundAutoPlayHandler = this.autoPlay.bind(this); protected _boundAutoPlayHandler = this.autoPlay.bind(this);
protected _boundAutoUnmuteHandler = this.autoUnmute.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); protected _boundTitleHandler = this._titleHandler.bind(this);
// This carousel may be resized by Lovelace resizes, window resizes, // This carousel may be resized by Lovelace resizes, window resizes,
// fullscreen, etc. Always call the adaptive height handler when the size // fullscreen, etc. Always call the adaptive height handler when the size
// changes. // changes.
protected _resizeObserver: ResizeObserver; protected _resizeObserver: ResizeObserver;
protected _slideResizeObserver: ResizeObserver;
protected _intersectionObserver: IntersectionObserver; protected _intersectionObserver: IntersectionObserver;
protected _refCarousel: Ref<FrigateCardCarousel> = createRef(); protected _refCarousel: Ref<FrigateCardCarousel> = createRef();
constructor() { constructor() {
super(); 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._intersectionObserver = new IntersectionObserver(
this._intersectionHandler.bind(this), this._intersectionHandler.bind(this),
); );
@@ -252,6 +257,14 @@ export class FrigateCardMediaCarousel extends LitElement {
super.disconnectedCallback(); 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. * Called when the carousel intersects with the viewport.
* @param entries The IntersectionObserverEntry entries (should be only 1). * @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: * - Example bug when this reinitialization is not performed:
* https://github.com/dermotduffy/frigate-hass-card/issues/651 * 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)) { if (entries.some((entry) => entry.isIntersecting)) {
// For performance, run the reinit in idle cycles if the browser supports this._reInitAndAdjustHeight();
// 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();
}
} }
} }
@@ -293,14 +290,16 @@ export class FrigateCardMediaCarousel extends LitElement {
* have changed. This handler is not triggered from carousel events, as it's * 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 * actually the media load/show that will change the dimensions, and that is
* async from carousel actions (e.g. lazy-loaded media). * 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 adaptCarouselHeight = (): void => {
const slide = this.frigateCardCarousel()?.carouselSelected(); const selected = this.frigateCardCarousel()?.getCarouselSelected();
if (slide !== undefined) { if (selected) {
this.style.removeProperty('max-height'); this.style.removeProperty('max-height');
const currentSlide = this.frigateCardCarousel()?.carouselSelectedElement(); const height = selected.element.getBoundingClientRect().height;
const height = currentSlide?.getBoundingClientRect().height;
if (height !== undefined && height > 0) { if (height !== undefined && height > 0) {
this.style.maxHeight = `${height}px`; this.style.maxHeight = `${height}px`;
} }
@@ -321,7 +320,7 @@ export class FrigateCardMediaCarousel extends LitElement {
* Fire a media show event when a slide is selected. * Fire a media show event when a slide is selected.
*/ */
protected _dispatchMediaShowInfo(): void { protected _dispatchMediaShowInfo(): void {
const slideIndex = this.frigateCardCarousel()?.carouselSelected(); const slideIndex = this.frigateCardCarousel()?.getCarouselSelected()?.index;
if (slideIndex !== undefined && slideIndex in this._mediaShowInfo) { if (slideIndex !== undefined && slideIndex in this._mediaShowInfo) {
dispatchExistingMediaShowInfoAsEvent(this, this._mediaShowInfo[slideIndex]); dispatchExistingMediaShowInfoAsEvent(this, this._mediaShowInfo[slideIndex]);
} }
@@ -344,7 +343,7 @@ export class FrigateCardMediaCarousel extends LitElement {
// rejected upstream (empty 1x1 images will be rejected here). // rejected upstream (empty 1x1 images will be rejected here).
if (mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) { if (mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) {
this._mediaShowInfo[slideIndex] = mediaShowInfo; this._mediaShowInfo[slideIndex] = mediaShowInfo;
if (this.frigateCardCarousel()?.carouselSelected() === slideIndex) { if (this.frigateCardCarousel()?.getCarouselSelected()?.index === slideIndex) {
dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo); dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo);
} }
} }
@@ -357,7 +356,11 @@ export class FrigateCardMediaCarousel extends LitElement {
.carouselPlugins=${this.carouselPlugins} .carouselPlugins=${this.carouselPlugins}
transitionEffect=${ifDefined(this.transitionEffect)} transitionEffect=${ifDefined(this.transitionEffect)}
@frigate-card:carousel:init=${this._dispatchMediaShowInfo.bind(this)} @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)} @frigate-card:carousel:media-show=${this._storeMediaShowInfo.bind(this)}
> >
<slot slot="previous" name="previous"></slot> <slot slot="previous" name="previous"></slot>
+1 -7
View File
@@ -80,13 +80,7 @@ export class FrigateCardThumbnailCarousel extends LitElement {
* Handle gallery resize. * Handle gallery resize.
*/ */
protected _resizeHandler(): void { protected _resizeHandler(): void {
this._refCarousel.value?.carouselReInit(); this._refCarousel.value?.carouselReInitWhenSafe();
// 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);
}
} }
/** /**
+6 -3
View File
@@ -206,7 +206,10 @@ export class FrigateCardViewerCarousel extends LitElement {
this.view.childIndex != oldView.childIndex this.view.childIndex != oldView.childIndex
) { ) {
const slide = this._getSlideForChild(this.view.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 // If the media target is the same as already loaded, but isn't of
// the selected slide, scroll to that slide. // the selected slide, scroll to that slide.
frigateCardCarousel.carouselScrollTo(slide); frigateCardCarousel.carouselScrollTo(slide);
@@ -265,7 +268,7 @@ export class FrigateCardViewerCarousel extends LitElement {
if (!slide) { if (!slide) {
slide = this._refMediaCarousel.value slide = this._refMediaCarousel.value
?.frigateCardCarousel() ?.frigateCardCarousel()
?.carouselSelectedElement(); ?.getCarouselSelected()?.element;
} }
return ( return (
@@ -462,7 +465,7 @@ export class FrigateCardViewerCarousel extends LitElement {
// Update the childIndex in the view. // Update the childIndex in the view.
const selected = this._refMediaCarousel.value const selected = this._refMediaCarousel.value
.frigateCardCarousel() .frigateCardCarousel()
?.carouselSelected(); ?.getCarouselSelected()?.index;
if (selected !== undefined) { if (selected !== undefined) {
const childIndex = this._slideToChild[selected]; const childIndex = this._slideToChild[selected];
if (childIndex !== undefined) { if (childIndex !== undefined) {