132 lines
4.2 KiB
TypeScript
132 lines
4.2 KiB
TypeScript
import { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
|
|
import { LazyUnloadCondition } from '../config/schema/common/media-actions';
|
|
|
|
type LazyLoadListener = (loaded: boolean) => void;
|
|
|
|
interface LazyLoadConfiguration {
|
|
// Whether to wait for the host to intersect (and the document to be visible)
|
|
// before loading. `false` loads eagerly on first call.
|
|
lazyLoad?: boolean;
|
|
|
|
// Conditions under which an already-loaded host should unload.
|
|
lazyUnloadConditions?: LazyUnloadCondition[];
|
|
|
|
// Treat the host as the selected/visible item, regardless of what
|
|
// IntersectionObserver reports.
|
|
forceSelected?: boolean;
|
|
}
|
|
|
|
export class LazyLoadController implements ReactiveController {
|
|
private _host: ReactiveControllerHost & HTMLElement;
|
|
private _documentVisible = true;
|
|
private _intersects = false;
|
|
private _forceSelected = false;
|
|
private _loaded = false;
|
|
private _unloadConditions: LazyUnloadCondition[] = [];
|
|
private _intersectionObserver = new IntersectionObserver(
|
|
this._intersectionHandler.bind(this),
|
|
);
|
|
private _listeners: LazyLoadListener[] = [];
|
|
|
|
constructor(host: ReactiveControllerHost & HTMLElement) {
|
|
this._host = host;
|
|
this._host.addController(this);
|
|
}
|
|
|
|
public setConfiguration(configuration: LazyLoadConfiguration): void {
|
|
this._unloadConditions = configuration.lazyUnloadConditions ?? [];
|
|
this._forceSelected = configuration.forceSelected ?? false;
|
|
|
|
// Eager-load fast path: skip re-evaluation so an immediately-applied
|
|
// `unselected` unload condition can't undo the eager load before the
|
|
// intersection observer has had a chance to fire.
|
|
if (configuration.lazyLoad === false && !this._loaded) {
|
|
this._setLoaded(true);
|
|
return;
|
|
}
|
|
this._lazyLoadOrUnloadIfNecessary();
|
|
}
|
|
|
|
public destroy(): void {
|
|
this._removeEventHandlers();
|
|
this._listeners = [];
|
|
}
|
|
|
|
public isLoaded(): boolean {
|
|
return this._loaded;
|
|
}
|
|
|
|
public addListener(listener: LazyLoadListener): void {
|
|
this._listeners.push(listener);
|
|
}
|
|
|
|
public removeListener(listener: LazyLoadListener): void {
|
|
this._listeners = this._listeners.filter((l) => l !== listener);
|
|
}
|
|
|
|
public removeController(): void {
|
|
this._host.removeController(this);
|
|
}
|
|
|
|
public hostConnected(): void {
|
|
// Capture the document's actual visibility state on connection. The
|
|
// `visibilitychange` listener only fires on transitions, so without this
|
|
// sync read a host that connects while the tab is already hidden would
|
|
// incorrectly believe the document is visible until the next transition.
|
|
this._documentVisible = document.visibilityState === 'visible';
|
|
this._addEventHandlers();
|
|
}
|
|
|
|
public hostDisconnected(): void {
|
|
this._removeEventHandlers();
|
|
this._setLoaded(false);
|
|
}
|
|
|
|
private _addEventHandlers(): void {
|
|
document.addEventListener('visibilitychange', this._visibilityHandler);
|
|
this._intersectionObserver.observe(this._host);
|
|
}
|
|
|
|
private _removeEventHandlers(): void {
|
|
document.removeEventListener('visibilitychange', this._visibilityHandler);
|
|
this._intersectionObserver.disconnect();
|
|
}
|
|
|
|
private _lazyLoadOrUnloadIfNecessary(): void {
|
|
const effectivelyIntersects = this._intersects || this._forceSelected;
|
|
const shouldBeLoaded =
|
|
!this._loaded && this._documentVisible && effectivelyIntersects;
|
|
const shouldBeUnloaded =
|
|
this._loaded &&
|
|
((this._unloadConditions.includes('hidden') && !this._documentVisible) ||
|
|
(this._unloadConditions.includes('unselected') && !effectivelyIntersects));
|
|
|
|
if (shouldBeLoaded) {
|
|
this._setLoaded(true);
|
|
} else if (shouldBeUnloaded) {
|
|
this._setLoaded(false);
|
|
}
|
|
}
|
|
|
|
private _setLoaded(loaded: boolean): void {
|
|
this._loaded = loaded;
|
|
this._notifyListeners();
|
|
this._host.requestUpdate();
|
|
}
|
|
|
|
private _notifyListeners(): void {
|
|
this._listeners.forEach((listener) => listener(this._loaded));
|
|
}
|
|
|
|
private _intersectionHandler(entries: IntersectionObserverEntry[]): void {
|
|
this._intersects = entries.some((entry) => entry.isIntersecting);
|
|
this._lazyLoadOrUnloadIfNecessary();
|
|
}
|
|
|
|
private _visibilityHandler = (): void => {
|
|
this._documentVisible = document.visibilityState === 'visible';
|
|
this._lazyLoadOrUnloadIfNecessary();
|
|
};
|
|
}
|