118 lines
3.6 KiB
TypeScript
118 lines
3.6 KiB
TypeScript
import { debounce, isEqual } from 'lodash-es';
|
|
|
|
export class MediaHeightController {
|
|
private _host: HTMLElement;
|
|
private _selector: string;
|
|
|
|
private _root: HTMLElement | DocumentFragment | null = null;
|
|
private _children: HTMLElement[] = [];
|
|
private _selectedChild: HTMLElement | null = null;
|
|
|
|
private _mutationObserver = new MutationObserver(() => this._initializeRoot());
|
|
private _resizeObserver = new ResizeObserver(() => this._debouncedSetHeight());
|
|
|
|
private _debouncedSetHeight = debounce(
|
|
() => this._setHeight(),
|
|
// Balancing act: Debounce to avoid excessive calls to setHeight, when new
|
|
// media is loading the player may be a much smaller height momentarily.
|
|
300,
|
|
{
|
|
trailing: true,
|
|
leading: false,
|
|
},
|
|
);
|
|
|
|
constructor(host: HTMLElement, selector: string) {
|
|
this._host = host;
|
|
this._selector = selector;
|
|
}
|
|
|
|
public setRoot(root: HTMLElement | DocumentFragment): void {
|
|
if (root === this._root) {
|
|
return;
|
|
}
|
|
|
|
this._root = root;
|
|
this._mutationObserver.disconnect();
|
|
this._mutationObserver.observe(this._root, {
|
|
childList: true,
|
|
});
|
|
this._initializeRoot();
|
|
}
|
|
|
|
public setSelected(selectedIndex: number): void {
|
|
const selectedChild: HTMLElement | undefined = this._children[selectedIndex];
|
|
if (!selectedChild || selectedChild === this._selectedChild) {
|
|
return;
|
|
}
|
|
this._selectedChild = selectedChild;
|
|
|
|
this._resizeObserver.disconnect();
|
|
this._resizeObserver.observe(selectedChild);
|
|
|
|
this._debouncedSetHeight();
|
|
}
|
|
|
|
// Recalculate the height. This is necessary because ResizeObserver may not
|
|
// fire if the height of the media is constrained by the host's own max-height
|
|
// (e.g. during initial load, everything is 100% height of the host, so the
|
|
// rendered height never changes and thus ResizeObserver never fires). This
|
|
// manual "wakeup" allows the controller to temporarily lift the constraint
|
|
// and peek at the true desired height.
|
|
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2109
|
|
public recalculate(): void {
|
|
this._debouncedSetHeight();
|
|
}
|
|
|
|
public destroy(): void {
|
|
this._mutationObserver.disconnect();
|
|
this._resizeObserver.disconnect();
|
|
|
|
this._root = null;
|
|
this._children = [];
|
|
this._selectedChild = null;
|
|
}
|
|
|
|
private _setHeight(): void {
|
|
if (!this._selectedChild) {
|
|
return;
|
|
}
|
|
|
|
const originalHeight = this._host.style.maxHeight;
|
|
|
|
// Remove the height restriction to ensure the full max height. Example of
|
|
// behavior without this: Chrome on Android will not correctly size if the
|
|
// card is in fullscreen mode.
|
|
this._host.style.maxHeight = '';
|
|
|
|
// Calculate the true height.
|
|
const selectedHeight = this._selectedChild.getBoundingClientRect().height;
|
|
|
|
// Reset the original height so that browser transition animation can be
|
|
// applied from the current to the target.
|
|
this._host.style.maxHeight = originalHeight;
|
|
|
|
// Force the browser to reflow.
|
|
this._selectedChild.getBoundingClientRect();
|
|
|
|
if (selectedHeight && !isNaN(selectedHeight) && selectedHeight > 0) {
|
|
this._host.style.maxHeight = `${selectedHeight}px`;
|
|
}
|
|
}
|
|
|
|
private _initializeRoot(): void {
|
|
const children = [
|
|
...(this._root?.querySelectorAll<HTMLElement>(this._selector) ??
|
|
/* istanbul ignore next: this path cannot be reached as root will always
|
|
exist by the time the mutation observer is observing -- @preserve */
|
|
[]),
|
|
];
|
|
if (isEqual(children, this._children)) {
|
|
return;
|
|
}
|
|
|
|
this._children = children;
|
|
this._selectedChild = null;
|
|
}
|
|
}
|