Improve reinit/resize handling.
This commit is contained in:
+67
-19
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user