Refactor carousel into separate components.

This commit is contained in:
Dermot Duffy
2022-07-09 22:43:43 -07:00
parent 30fec98e18
commit dedecc24c6
18 changed files with 698 additions and 517 deletions
+156 -38
View File
@@ -1,10 +1,20 @@
import EmblaCarousel, {
EmblaCarouselType,
EmblaOptionsType,
EmblaPluginType
} from 'embla-carousel';
import { CSSResultGroup, LitElement, PropertyValues, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel';
import { EmblaNodesType } from 'embla-carousel/components';
import {
CreatePluginType,
EmblaPluginsType,
LoosePluginType,
} from 'embla-carousel/components/Plugins';
import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
unsafeCSS,
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
import carouselStyle from '../scss/carousel.scss';
import { TransitionEffect } from '../types';
import { dispatchFrigateCardEvent } from '../utils/basic.js';
@@ -13,28 +23,120 @@ export interface CarouselSelect {
index: number;
}
export type EmblaCarouselPlugins = CreatePluginType<
LoosePluginType,
Record<string, unknown>
>[];
@customElement('frigate-card-carousel')
export class FrigateCardCarousel extends LitElement {
@property({ attribute: true, reflect: true })
public direction: 'vertical' | 'horizontal' = 'horizontal';
@property({ attribute: false })
public carouselOptions?: EmblaOptionsType;
@property({ attribute: false })
public carouselPlugins?: EmblaCarouselPlugins;
@property({ attribute: true })
public transitionEffect?: TransitionEffect;
protected _refSlot: Ref<HTMLSlotElement> = createRef();
protected _carousel?: EmblaCarouselType;
connectedCallback(): void {
super.connectedCallback();
// Guarantee a re-render if the component is reconnected. See note in
// disconnectedCallback().
this.requestUpdate();
}
/**
* Component disconnected callback.
*/
disconnectedCallback(): void {
// Destroy the carousel when the component is disconnected, which forces the
// plugins (which may have registered event handlers) to also be destroyed.
// The carousel will automatically reconstruct if the component is re-rendered.
this._destroyCarousel();
super.disconnectedCallback();
}
/**
* Scroll to a particular slide.
* @param index Slide number.
*/
carouselScrollTo(index: number): void {
this._carousel?.scrollTo(index, this._getTransitionEffect() === 'none');
public carouselScrollTo(index: number): void {
this._carousel?.scrollTo(index, this.transitionEffect === 'none');
}
/**
* Scroll to the previous slide.
*/
public carouselScrollPrevious(): void {
this._carousel?.scrollPrev(this.transitionEffect === 'none');
}
/**
* Scroll to the next slide.
*/
public carouselScrollNext(): void {
this._carousel?.scrollNext(this.transitionEffect === 'none');
}
/**
* Get the selected slide.
* @returns The slide index or undefined if the carousel is not loaded.
*/
carouselSelected(): number | undefined {
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;
}
return null;
}
/**
* Get the carousel.
*/
public carouselClickAllowed(): boolean {
return this._carousel?.clickAllowed() ?? true;
}
/**
* Get the carousel.
*/
public carousel(): EmblaCarouselType | null {
return this._carousel ?? null;
}
/**
* ReInit the carousel.
*/
public carouselReInit(): void {
// Safari appears to not loop the carousel unless the options are passed
// back in during re-initialization.
return this._carousel?.reInit(this.carouselOptions);
}
/**
* Get the live carousel plugins.
*/
public getCarouselPlugins(): EmblaPluginsType | null {
return this._carousel?.plugins() ?? null;
}
/**
* The updated lifecycle callback for this element.
* @param changedProperties The properties that were changed in this render.
@@ -52,30 +154,6 @@ export class FrigateCardCarousel extends LitElement {
}
}
/**
* Get the transition effect to use.
* @returns An TransitionEffect object.
*/
protected _getTransitionEffect(): TransitionEffect | undefined {
return 'slide';
}
/**
* Get the Embla options to use.
* @returns An EmblaOptionsType object or undefined for no options.
*/
protected _getOptions(): EmblaOptionsType | undefined {
return undefined;
}
/**
* Get the Embla plugins to use.
* @returns A list of EmblaOptionsTypes.
*/
protected _getPlugins(): EmblaPluginType[] {
return [];
}
protected _destroyCarousel(): void {
if (this._carousel) {
this._carousel.destroy();
@@ -91,14 +169,21 @@ export class FrigateCardCarousel extends LitElement {
'.embla__viewport',
) as HTMLElement;
if (carouselNode) {
const nodes: EmblaNodesType = {
root: carouselNode,
// As the slides are slotted, need to explicitly pull them out and pass
// them to Embla.
slides: this._refSlot.value?.assignedElements({ flatten: true }) as HTMLElement[],
};
if (carouselNode && nodes.slides) {
this._carousel = EmblaCarousel(
carouselNode,
nodes,
{
axis: this.direction == 'horizontal' ? 'x' : 'y',
...this._getOptions(),
...this.carouselOptions,
},
this._getPlugins() ?? [],
this.carouselPlugins,
);
this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init'));
this._carousel.on('select', () => {
@@ -112,6 +197,33 @@ export class FrigateCardCarousel extends LitElement {
}
}
/**
* Called when the slotted children in the carousel change.
*/
protected _slotChanged(): void {
// Cannot just re-init, because the slide elements themselves may have
// changed, and only a carousel init can pass in new (slotted) children.
this._destroyCarousel();
this.requestUpdate();
}
protected render(): TemplateResult | void {
const slides = this._refSlot.value?.assignedElements({ flatten: true }) || [];
const currentSlide = this._carousel?.selectedScrollSnap() ?? 0;
const showPrevious = this.carouselOptions?.loop || currentSlide > 0;
const showNext = this.carouselOptions?.loop || currentSlide + 1 < slides.length;
return html` <div class="embla">
${showPrevious ? html`<slot name="previous"></slot>` : ``}
<div class="embla__viewport">
<div class="embla__container">
<slot ${ref(this._refSlot)} @slotchange=${this._slotChanged}></slot>
</div>
</div>
${showNext ? html`<slot name="next"></slot>` : ``}
</div>`;
}
/**
* Get element styles.
*/
@@ -119,3 +231,9 @@ export class FrigateCardCarousel extends LitElement {
return unsafeCSS(carouselStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'frigate-card-carousel': FrigateCardCarousel;
}
}