@@ -135,6 +135,11 @@ cameras:
|
|||||||
| -------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `aspect_ratio` | | An optional aspect ratio for media from this camera which will be used in `live` or media viewer related views (e.g. `clip`, `snapshot` and `recording`). Format is the same as the parameter of the same name under the [dimensions block](../dimensions.md) (which controls dimensions for the whole card), e.g. `16 / 9`. |
|
| `aspect_ratio` | | An optional aspect ratio for media from this camera which will be used in `live` or media viewer related views (e.g. `clip`, `snapshot` and `recording`). Format is the same as the parameter of the same name under the [dimensions block](../dimensions.md) (which controls dimensions for the whole card), e.g. `16 / 9`. |
|
||||||
| `layout` | | How the media should be laid out _within_ the camera dimensions. See below. |
|
| `layout` | | How the media should be laid out _within_ the camera dimensions. See below. |
|
||||||
|
| `rotation` | `0` | Rotates the camera clockwise by `0`, `90`, `180` or `270` degrees. |
|
||||||
|
|
||||||
|
?> Use of `rotation` causes the browser to rotate the video player, unavoidably _including_ rotating the builtin video controls on the player, which may be distracting or confusing (e.g. upside down controls). Builtin controls can be disabled using the [`live.controls.builtin` parameter](../live.md?id=controls). Rotation is not available in iOS fullscreen, due to the limited fullscreen support offered by that OS.
|
||||||
|
|
||||||
|
!> Rotating the camera incurs a rendering performance penalty. Always rotate "upstream" if possible (e.g. in your camera settings).
|
||||||
|
|
||||||
### Layout Configuration
|
### Layout Configuration
|
||||||
|
|
||||||
@@ -182,6 +187,15 @@ See [media layout examples](../../examples.md?id=media-layout).
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### Order of Operations
|
||||||
|
|
||||||
|
Camera `dimensions` settings are applied in this order:
|
||||||
|
|
||||||
|
- `aspect_ratio` defines the aspect ratio of the video player ...
|
||||||
|
- ... then `fit`, `position` and `view_box` defines how the media is laid out within that ratio ...
|
||||||
|
- ... then `rotation` defines whether the video is rotated ...
|
||||||
|
- ... then `zoom` and `pan` define the zoom and pan settings respectively.
|
||||||
|
|
||||||
## `ptz`
|
## `ptz`
|
||||||
|
|
||||||
Configure the PTZ actions taken for a camera (not to be confused with configuration of the PTZ _controls_, see [Live PTZ Controls](../live.md?id=ptz) or [Media Viewer PTZ Controls](../media-viewer.md?id=ptz)). Manually configured actions override any auto-detected actions.
|
Configure the PTZ actions taken for a camera (not to be confused with configuration of the PTZ _controls_, see [Live PTZ Controls](../live.md?id=ptz) or [Media Viewer PTZ Controls](../media-viewer.md?id=ptz)). Manually configured actions override any auto-detected actions.
|
||||||
@@ -437,6 +451,9 @@ cameras:
|
|||||||
- trigger
|
- trigger
|
||||||
disable:
|
disable:
|
||||||
# Capabilities to selectively disable.
|
# Capabilities to selectively disable.
|
||||||
|
- camera_entity: camera.rotated
|
||||||
|
dimensions:
|
||||||
|
rotation: 90
|
||||||
cameras_global:
|
cameras_global:
|
||||||
triggers:
|
triggers:
|
||||||
motion: false
|
motion: false
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ This card supports several menu styles.
|
|||||||
| `none` | No status bar is shown. |
|
| `none` | No status bar is shown. |
|
||||||
| `outside` | Render the status bar outside the card (i.e. above it if `position` is `top`, or below it if `position` is `bottom`). |
|
| `outside` | Render the status bar outside the card (i.e. above it if `position` is `top`, or below it if `position` is `bottom`). |
|
||||||
| `overlay` | Overlay the status bar over the card contents. |
|
| `overlay` | Overlay the status bar over the card contents. |
|
||||||
|
| `popup` | Equivalent to `overlay` except the status bar disappears after `popup_seconds`. |
|
||||||
|
|
||||||
## Fully expanded reference
|
## Fully expanded reference
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,346 @@
|
|||||||
|
import { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
import { CameraDimensionsConfig } from '../config/schema/cameras';
|
||||||
|
import { MediaLoadedInfo } from '../types';
|
||||||
|
import {
|
||||||
|
aspectRatioToString,
|
||||||
|
setOrRemoveAttribute,
|
||||||
|
setOrRemoveStyleProperty,
|
||||||
|
} from '../utils/basic';
|
||||||
|
import { AdvancedCameraCardMediaLoadedEventTarget } from '../utils/media-info';
|
||||||
|
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout';
|
||||||
|
|
||||||
|
const ROTATED_ATTRIBUTE = 'rotated';
|
||||||
|
|
||||||
|
interface MediaDimensions {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for managing media dimensions in a container. This accepts two
|
||||||
|
* containers (inner and outer). The inner container is expected to contain the
|
||||||
|
* media itself, the outer container is used to change the height that the inner
|
||||||
|
* container is allowed to be. This is necessary since when the inner container
|
||||||
|
* is rotated, the outer container will already have been sized by browser
|
||||||
|
* ignoring the rotation -- so the outer container has its height manually set
|
||||||
|
* based on the expected rotation height. The host itself (this element) needs
|
||||||
|
* to not have a fixed height, in order for the ResizeObserver to work
|
||||||
|
* correctly, necessitating the use of a special outer container.
|
||||||
|
*/
|
||||||
|
export class MediaDimensionsContainerController implements ReactiveController {
|
||||||
|
private _host: HTMLElement & ReactiveControllerHost;
|
||||||
|
|
||||||
|
private _dimensionsConfig: CameraDimensionsConfig | null = null;
|
||||||
|
|
||||||
|
private _innerContainer:
|
||||||
|
| (HTMLElement & AdvancedCameraCardMediaLoadedEventTarget)
|
||||||
|
| null = null;
|
||||||
|
private _outerContainer: HTMLElement | null = null;
|
||||||
|
|
||||||
|
public resize = debounce(this._resize.bind(this), 100, { trailing: true });
|
||||||
|
private _resizeObserver = new ResizeObserver(this.resize);
|
||||||
|
|
||||||
|
private _mediaDimensions: MediaDimensions | null = null;
|
||||||
|
|
||||||
|
constructor(host: HTMLElement & ReactiveControllerHost) {
|
||||||
|
this._host = host;
|
||||||
|
this._host.addController(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public hostConnected(): void {
|
||||||
|
this._resizeObserver.observe(this._host);
|
||||||
|
this._addInnerContainerListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
public hostDisconnected(): void {
|
||||||
|
this._resizeObserver.disconnect();
|
||||||
|
this._removeInnerContainerListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeInnerContainerListeners(): void {
|
||||||
|
if (!this._innerContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._innerContainer.removeEventListener('slotchange', this.resize);
|
||||||
|
this._innerContainer.removeEventListener(
|
||||||
|
'advanced-camera-card:media:loaded',
|
||||||
|
this._mediaLoadedHandler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addInnerContainerListeners(): void {
|
||||||
|
if (!this._host.isConnected || !this._innerContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._innerContainer.addEventListener('slotchange', this.resize);
|
||||||
|
this._innerContainer.addEventListener(
|
||||||
|
'advanced-camera-card:media:loaded',
|
||||||
|
this._mediaLoadedHandler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _mediaLoadedHandler = (ev: CustomEvent<MediaLoadedInfo>): void => {
|
||||||
|
// Only resize if the media dimensions have changed (otherwise the loading
|
||||||
|
// image whilst waiting for the stream, will trigger aresize every second).
|
||||||
|
if (
|
||||||
|
this._mediaDimensions?.width === ev.detail.width &&
|
||||||
|
this._mediaDimensions.height === ev.detail.height
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._mediaDimensions = {
|
||||||
|
width: ev.detail.width,
|
||||||
|
height: ev.detail.height,
|
||||||
|
};
|
||||||
|
this.resize();
|
||||||
|
};
|
||||||
|
|
||||||
|
public setConfig(dimensionsConfig?: CameraDimensionsConfig): void {
|
||||||
|
if (dimensionsConfig === this._dimensionsConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dimensionsConfig = dimensionsConfig ?? null;
|
||||||
|
this._setInnerContainerProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setContainers(
|
||||||
|
innerContainer?: HTMLElement,
|
||||||
|
outerContainer?: HTMLElement,
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
(innerContainer ?? null) === this._innerContainer &&
|
||||||
|
(outerContainer ?? null) === this._outerContainer
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._removeInnerContainerListeners();
|
||||||
|
|
||||||
|
this._innerContainer = innerContainer ?? null;
|
||||||
|
this._outerContainer = outerContainer ?? null;
|
||||||
|
|
||||||
|
this._addInnerContainerListeners();
|
||||||
|
this._setInnerContainerProperties();
|
||||||
|
this._resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hasFixedAspectRatio(): boolean {
|
||||||
|
return this._dimensionsConfig?.aspect_ratio?.length === 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _requiresRotation(): boolean {
|
||||||
|
return !!this._dimensionsConfig?.rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _requiresContainerRotation(): boolean {
|
||||||
|
// The actual container only needs to rotate if the rotation parameter is 90
|
||||||
|
// or 270.
|
||||||
|
return (
|
||||||
|
this._dimensionsConfig?.rotation === 90 || this._dimensionsConfig?.rotation === 270
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setInnerContainerProperties(): void {
|
||||||
|
if (!this._innerContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrRemoveStyleProperty(
|
||||||
|
this._innerContainer,
|
||||||
|
this._requiresRotation(),
|
||||||
|
'--advanced-camera-card-media-rotation',
|
||||||
|
`${this._dimensionsConfig?.rotation}deg`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this._innerContainer.style.aspectRatio = aspectRatioToString({
|
||||||
|
ratio: this._dimensionsConfig?.aspect_ratio,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateElementStyleFromMediaLayoutConfig(
|
||||||
|
this._innerContainer,
|
||||||
|
this._dimensionsConfig?.layout,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============
|
||||||
|
// Resize helpers
|
||||||
|
// ==============
|
||||||
|
|
||||||
|
private _setMaxSize = (element: HTMLElement): void =>
|
||||||
|
this._setSize(element, {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
private _setIntrinsicSize = (element: HTMLElement): void =>
|
||||||
|
this._setSize(element, {
|
||||||
|
width: 'max-content',
|
||||||
|
height: 'max-content',
|
||||||
|
});
|
||||||
|
|
||||||
|
private _setWidthBoundIntrinsicSize = (element: HTMLElement): void =>
|
||||||
|
this._setSize(element, {
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
private _setHeightBoundIntrinsicSize = (element: HTMLElement): void =>
|
||||||
|
this._setSize(element, {
|
||||||
|
width: 'auto',
|
||||||
|
height: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
private _setInvisible = (element: HTMLElement): void => {
|
||||||
|
element.style.visibility = 'hidden';
|
||||||
|
};
|
||||||
|
|
||||||
|
private _setVisible = (element: HTMLElement): void => {
|
||||||
|
element.style.visibility = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
private _setSize(
|
||||||
|
element: HTMLElement,
|
||||||
|
options: { width?: number | string; height: number | string },
|
||||||
|
): void {
|
||||||
|
const toCSS = (value: number | string): string =>
|
||||||
|
typeof value === 'number' ? `${value}px` : value;
|
||||||
|
|
||||||
|
if (options.width !== undefined) {
|
||||||
|
element.style.width = toCSS(options.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.style.height = toCSS(options.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setRotation(element: HTMLElement, rotate: boolean): void {
|
||||||
|
setOrRemoveAttribute(element, rotate, ROTATED_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resize(): void {
|
||||||
|
if (this._requiresRotation()) {
|
||||||
|
this._resizeAndRotate();
|
||||||
|
} else if (this._hasFixedAspectRatio()) {
|
||||||
|
this._resizeWithFixedAspectRatio();
|
||||||
|
} else {
|
||||||
|
this._resizeDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resizeDefault(): void {
|
||||||
|
if (!this._innerContainer || !this._outerContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._setMaxSize(this._innerContainer);
|
||||||
|
this._setMaxSize(this._outerContainer);
|
||||||
|
this._setRotation(this._host, false);
|
||||||
|
this._setVisible(this._innerContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resizeWithFixedAspectRatio(): void {
|
||||||
|
if (!this._innerContainer || !this._outerContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._setInvisible(this._innerContainer);
|
||||||
|
|
||||||
|
this._setWidthBoundIntrinsicSize(this._innerContainer);
|
||||||
|
this._setMaxSize(this._outerContainer);
|
||||||
|
this._setRotation(this._host, false);
|
||||||
|
|
||||||
|
const hostSize = this._host.getBoundingClientRect();
|
||||||
|
const innerContainerSize = this._innerContainer.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (this._resizeDefaultIfInvalidSizes([hostSize, innerContainerSize])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the container is larger than the host, the host was not able to expand
|
||||||
|
// enough to cover the size (e.g. fullscreen, panel or height constrained in
|
||||||
|
// configuration). In this case, just limit the container to the host height
|
||||||
|
// at the same aspect ratio.
|
||||||
|
if (innerContainerSize.height > hostSize.height) {
|
||||||
|
this._setHeightBoundIntrinsicSize(this._innerContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._setVisible(this._innerContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hasValidSize(size: DOMRect): boolean {
|
||||||
|
return size.width > 0 && size.height > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resizeDefaultIfInvalidSizes(sizes: DOMRect[]): boolean {
|
||||||
|
if (sizes.some((size) => !this._hasValidSize(size))) {
|
||||||
|
this._resizeDefault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resizeAndRotate(): void {
|
||||||
|
if (!this._innerContainer || !this._outerContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._requiresContainerRotation()) {
|
||||||
|
this._resizeDefault();
|
||||||
|
this._setRotation(this._host, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._setInvisible(this._innerContainer);
|
||||||
|
|
||||||
|
// Render the media entirely unhindered to get the native aspect ratio.
|
||||||
|
this._setIntrinsicSize(this._innerContainer);
|
||||||
|
this._setMaxSize(this._outerContainer);
|
||||||
|
this._setRotation(this._host, false);
|
||||||
|
|
||||||
|
let hostSize = this._host.getBoundingClientRect();
|
||||||
|
let innerContainerSize = this._innerContainer.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (this._resizeDefaultIfInvalidSizes([hostSize, innerContainerSize])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aspectRatio = innerContainerSize.width / innerContainerSize.height;
|
||||||
|
|
||||||
|
this._setRotation(this._host, true);
|
||||||
|
|
||||||
|
// Set the inner container to the correct rotated sizes (ignoring any
|
||||||
|
// constraint of host size).
|
||||||
|
this._setSize(this._innerContainer, {
|
||||||
|
width: hostSize.width * aspectRatio,
|
||||||
|
height: hostSize.width,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._setSize(this._outerContainer, {
|
||||||
|
height: hostSize.width * aspectRatio,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh the sizes post rotation & initial sizing.
|
||||||
|
innerContainerSize = this._innerContainer.getBoundingClientRect();
|
||||||
|
hostSize = this._host.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (this._resizeDefaultIfInvalidSizes([hostSize, innerContainerSize])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// As in `_resizeWithFixedAspectRatio` resize the media if the host was not
|
||||||
|
// able to expand to cover the size.
|
||||||
|
if (innerContainerSize.height > hostSize.height) {
|
||||||
|
this._setSize(this._innerContainer, {
|
||||||
|
width: hostSize.height,
|
||||||
|
height: hostSize.height / aspectRatio,
|
||||||
|
});
|
||||||
|
this._setSize(this._outerContainer, {
|
||||||
|
height: hostSize.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._setVisible(this._innerContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
||||||
import { throttle } from 'lodash-es';
|
|
||||||
import { CameraDimensionsConfig } from '../config/schema/cameras';
|
|
||||||
import { MediaLoadedInfo } from '../types';
|
|
||||||
import { aspectRatioToString, setOrRemoveAttribute } from '../utils/basic';
|
|
||||||
import { AdvancedCameraCardMediaLoadedEventTarget } from '../utils/media-info';
|
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout';
|
|
||||||
|
|
||||||
const SIZE_ATTRIBUTE = 'size';
|
|
||||||
type SizeMode = 'custom' | 'max' | 'max-height' | 'max-width';
|
|
||||||
|
|
||||||
export class MediaProviderDimensionsController implements ReactiveController {
|
|
||||||
public resize = throttle(this._resizeHandler.bind(this), 100, {
|
|
||||||
trailing: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
private _host: HTMLElement &
|
|
||||||
ReactiveControllerHost &
|
|
||||||
AdvancedCameraCardMediaLoadedEventTarget;
|
|
||||||
private _container: HTMLElement | null = null;
|
|
||||||
private _cameraConfig: CameraDimensionsConfig | null = null;
|
|
||||||
private _resizeObserver = new ResizeObserver(this.resize);
|
|
||||||
private _intendedHostSize: DOMRect | null = null;
|
|
||||||
|
|
||||||
constructor(host: HTMLElement & ReactiveControllerHost) {
|
|
||||||
this._host = host;
|
|
||||||
this._host.addController(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public hostConnected(): void {
|
|
||||||
this._host.addEventListener(
|
|
||||||
'advanced-camera-card:media:loaded',
|
|
||||||
this._mediaLoadHandler,
|
|
||||||
);
|
|
||||||
|
|
||||||
this._resizeObserver.observe(this._host);
|
|
||||||
}
|
|
||||||
|
|
||||||
public hostDisconnected(): void {
|
|
||||||
this._host.removeEventListener(
|
|
||||||
'advanced-camera-card:media:loaded',
|
|
||||||
this._mediaLoadHandler,
|
|
||||||
);
|
|
||||||
this._resizeObserver.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
public setContainer(container?: HTMLElement): void {
|
|
||||||
if (container === this._container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._container = container ?? null;
|
|
||||||
this._setAttributesFromConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setAttributesFromConfig(): void {
|
|
||||||
if (this._container) {
|
|
||||||
this._container.style.aspectRatio = aspectRatioToString({
|
|
||||||
ratio: this._cameraConfig?.aspect_ratio,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateElementStyleFromMediaLayoutConfig(this._host, this._cameraConfig?.layout);
|
|
||||||
|
|
||||||
// When the provider is not precisely sized, we guess the best aspect
|
|
||||||
// ratio to "maximize" if known. This prevents media "hopping" from no
|
|
||||||
// forced aspect ratio to a forced one, once its true size is known.
|
|
||||||
setOrRemoveAttribute<SizeMode>(
|
|
||||||
this._host,
|
|
||||||
true,
|
|
||||||
SIZE_ATTRIBUTE,
|
|
||||||
this._cameraConfig?.aspect_ratio
|
|
||||||
? this._cameraConfig?.aspect_ratio[0] >= this._cameraConfig?.aspect_ratio[1]
|
|
||||||
? 'max-width'
|
|
||||||
: 'max-height'
|
|
||||||
: 'max',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setCameraConfig(config?: CameraDimensionsConfig): void {
|
|
||||||
this._cameraConfig = config ?? null;
|
|
||||||
this._setAttributesFromConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
private _mediaLoadHandler = (_ev: CustomEvent<MediaLoadedInfo>): void => {
|
|
||||||
// Allow the browser to render the media fully before attempting to resize.
|
|
||||||
// Without this, viewer provider will not be sized correctly.
|
|
||||||
window.requestAnimationFrame(() => this.resize());
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
private _resizeHandler(_entries?: ResizeObserverEntry[]): void {
|
|
||||||
// Custom sizing only applies if the aspect ratio is set.
|
|
||||||
if (!this._cameraConfig?.aspect_ratio?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rememberHostSize = (): void => {
|
|
||||||
this._intendedHostSize = this._host.getBoundingClientRect();
|
|
||||||
};
|
|
||||||
|
|
||||||
const setMaxAttribute = (): void => {
|
|
||||||
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'max');
|
|
||||||
};
|
|
||||||
|
|
||||||
const setContainerIntrinsicSize = (container: HTMLElement): void => {
|
|
||||||
container.style.width = '100%';
|
|
||||||
container.style.height = 'auto';
|
|
||||||
rememberHostSize();
|
|
||||||
};
|
|
||||||
|
|
||||||
const setContainerSize = (
|
|
||||||
container: HTMLElement,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
): void => {
|
|
||||||
container.style.width = `${width}px`;
|
|
||||||
container.style.height = `${height}px`;
|
|
||||||
rememberHostSize();
|
|
||||||
};
|
|
||||||
|
|
||||||
const hostSize = this._host.getBoundingClientRect();
|
|
||||||
if (
|
|
||||||
hostSize.width === this._intendedHostSize?.width &&
|
|
||||||
hostSize.height === this._intendedHostSize?.height
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._container) {
|
|
||||||
setMaxAttribute();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In the ideal case, the width can be maximum and the height can be
|
|
||||||
// whatever is necessary to support the aspect ratio.
|
|
||||||
setContainerIntrinsicSize(this._container);
|
|
||||||
|
|
||||||
const containerSize = this._container.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (!containerSize.width || !containerSize.height) {
|
|
||||||
setMaxAttribute();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mediaAspectRatio = containerSize.width / containerSize.height;
|
|
||||||
const newHostSize = this._host.getBoundingClientRect();
|
|
||||||
|
|
||||||
// If the container is larger than the host, the host was not able to expand
|
|
||||||
// enough to cover the size (e.g. fullscreen, panel or height constrained in
|
|
||||||
// configuration). In this case, just limit the container to the host height
|
|
||||||
// at the same aspect ratio.
|
|
||||||
if (containerSize.height > newHostSize.height) {
|
|
||||||
setContainerSize(
|
|
||||||
this._container,
|
|
||||||
newHostSize.height * mediaAspectRatio,
|
|
||||||
newHostSize.height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'custom');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+30
-54
@@ -1,17 +1,9 @@
|
|||||||
import {
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
TemplateResult,
|
|
||||||
unsafeCSS,
|
|
||||||
} from 'lit';
|
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { guard } from 'lit/directives/guard.js';
|
import { guard } from 'lit/directives/guard.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
import { ViewManagerEpoch } from '../card-controller/view/types';
|
import { ViewManagerEpoch } from '../card-controller/view/types';
|
||||||
import { MediaProviderDimensionsController } from '../components-lib/media-provider-dimensions-controller';
|
|
||||||
import { ZoomSettingsObserved } from '../components-lib/zoom/types';
|
import { ZoomSettingsObserved } from '../components-lib/zoom/types';
|
||||||
import { handleZoomSettingsObservedEvent } from '../components-lib/zoom/zoom-view-context';
|
import { handleZoomSettingsObservedEvent } from '../components-lib/zoom/zoom-view-context';
|
||||||
import { CameraConfig } from '../config/schema/cameras';
|
import { CameraConfig } from '../config/schema/cameras';
|
||||||
@@ -22,6 +14,7 @@ import imageStyle from '../scss/image.scss';
|
|||||||
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types.js';
|
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types.js';
|
||||||
import './image-updating-player';
|
import './image-updating-player';
|
||||||
import { resolveImageMode } from './image-updating-player';
|
import { resolveImageMode } from './image-updating-player';
|
||||||
|
import './media-dimensions-container';
|
||||||
import './zoomer.js';
|
import './zoomer.js';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-image')
|
@customElement('advanced-camera-card-image')
|
||||||
@@ -41,28 +34,13 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public imageConfig?: ImageViewConfig;
|
public imageConfig?: ImageViewConfig;
|
||||||
|
|
||||||
protected _dimensionsController = new MediaProviderDimensionsController(this);
|
|
||||||
protected _refImage: Ref<MediaPlayerElement> = createRef();
|
protected _refImage: Ref<MediaPlayerElement> = createRef();
|
||||||
protected _refContainer: Ref<HTMLElement> = createRef();
|
|
||||||
|
|
||||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
return (await this._refImage.value?.getMediaPlayerController()) ?? null;
|
return (await this._refImage.value?.getMediaPlayerController()) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
|
||||||
if (changedProps.has('cameraConfig') || changedProps.has('imageConfig')) {
|
|
||||||
this._dimensionsController.setCameraConfig(
|
|
||||||
resolveImageMode({
|
|
||||||
imageConfig: this.imageConfig,
|
|
||||||
cameraConfig: this.cameraConfig,
|
|
||||||
}) === 'camera'
|
|
||||||
? this.cameraConfig?.dimensions
|
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _renderContainer(template: TemplateResult): TemplateResult {
|
protected _renderContainer(template: TemplateResult): TemplateResult {
|
||||||
const zoomTarget = IMAGE_VIEW_ZOOM_TARGET_SENTINEL;
|
const zoomTarget = IMAGE_VIEW_ZOOM_TARGET_SENTINEL;
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
@@ -71,33 +49,35 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
|||||||
cameraConfig: this.cameraConfig,
|
cameraConfig: this.cameraConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
return html`<div class="container" ${ref(this._refContainer)}>
|
const intermediateTemplate = html` <advanced-camera-card-media-dimensions-container
|
||||||
${this.imageConfig?.zoomable
|
.dimensionsConfig=${mode === 'camera' ? this.cameraConfig?.dimensions : undefined}
|
||||||
? html`<advanced-camera-card-zoomer
|
>
|
||||||
.defaultSettings=${guard(
|
${template}
|
||||||
[this.imageConfig, this.cameraConfig?.dimensions?.layout],
|
</advanced-camera-card-media-dimensions-container>`;
|
||||||
() =>
|
|
||||||
mode === 'camera' && this.cameraConfig?.dimensions?.layout
|
return html` ${this.imageConfig?.zoomable
|
||||||
? {
|
? html`<advanced-camera-card-zoomer
|
||||||
pan: this.cameraConfig.dimensions.layout.pan,
|
.defaultSettings=${guard(
|
||||||
zoom: this.cameraConfig.dimensions.layout.zoom,
|
[this.imageConfig, this.cameraConfig?.dimensions?.layout],
|
||||||
}
|
() =>
|
||||||
: undefined,
|
mode === 'camera' && this.cameraConfig?.dimensions?.layout
|
||||||
|
? {
|
||||||
|
pan: this.cameraConfig.dimensions.layout.pan,
|
||||||
|
zoom: this.cameraConfig.dimensions.layout.zoom,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
)}
|
||||||
|
.settings=${view?.context?.zoom?.[zoomTarget]?.requested}
|
||||||
|
@advanced-camera-card:zoom:change=${(ev: CustomEvent<ZoomSettingsObserved>) =>
|
||||||
|
handleZoomSettingsObservedEvent(
|
||||||
|
ev,
|
||||||
|
this.viewManagerEpoch?.manager,
|
||||||
|
zoomTarget,
|
||||||
)}
|
)}
|
||||||
.settings=${view?.context?.zoom?.[zoomTarget]?.requested}
|
>
|
||||||
@advanced-camera-card:zoom:change=${(
|
${intermediateTemplate}
|
||||||
ev: CustomEvent<ZoomSettingsObserved>,
|
</advanced-camera-card-zoomer>`
|
||||||
) =>
|
: intermediateTemplate}`;
|
||||||
handleZoomSettingsObservedEvent(
|
|
||||||
ev,
|
|
||||||
this.viewManagerEpoch?.manager,
|
|
||||||
zoomTarget,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
${template}
|
|
||||||
</advanced-camera-card-zoomer>`
|
|
||||||
: template}
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
@@ -117,10 +97,6 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updated(): void {
|
|
||||||
this._dimensionsController.setContainer(this._refContainer.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(imageStyle);
|
return unsafeCSS(imageStyle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { CameraEndpoints } from '../../camera-manager/types.js';
|
|||||||
import { MicrophoneState } from '../../card-controller/types.js';
|
import { MicrophoneState } from '../../card-controller/types.js';
|
||||||
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
||||||
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { MediaProviderDimensionsController } from '../../components-lib/media-provider-dimensions-controller.js';
|
|
||||||
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
|
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
|
||||||
import { CameraConfig, LiveProvider } from '../../config/schema/cameras.js';
|
import { CameraConfig, LiveProvider } from '../../config/schema/cameras.js';
|
||||||
import { LiveConfig } from '../../config/schema/live.js';
|
import { LiveConfig } from '../../config/schema/live.js';
|
||||||
@@ -32,6 +31,7 @@ import {
|
|||||||
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
||||||
import '../icon.js';
|
import '../icon.js';
|
||||||
import { renderMessage } from '../message.js';
|
import { renderMessage } from '../message.js';
|
||||||
|
import './../media-dimensions-container';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-live-provider')
|
@customElement('advanced-camera-card-live-provider')
|
||||||
export class AdvancedCameraCardLiveProvider extends LitElement implements MediaPlayer {
|
export class AdvancedCameraCardLiveProvider extends LitElement implements MediaPlayer {
|
||||||
@@ -70,9 +70,8 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
protected _showStreamTroubleshooting = false;
|
protected _showStreamTroubleshooting = false;
|
||||||
|
|
||||||
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||||
protected _refContainer: Ref<HTMLElement> = createRef();
|
|
||||||
protected _lazyLoadController: LazyLoadController = new LazyLoadController(this);
|
protected _lazyLoadController: LazyLoadController = new LazyLoadController(this);
|
||||||
protected _dimensionsController = new MediaProviderDimensionsController(this);
|
|
||||||
|
|
||||||
// A note on dynamic imports:
|
// A note on dynamic imports:
|
||||||
//
|
//
|
||||||
@@ -82,8 +81,8 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
// underlying code is not yet loaded.
|
// underlying code is not yet loaded.
|
||||||
//
|
//
|
||||||
// Test case: A card with a non-live view, but live pre-loaded, attempts to
|
// Test case: A card with a non-live view, but live pre-loaded, attempts to
|
||||||
// call mute() when the <advanced-camera-card-live> element first renders in the
|
// call mute() when the <advanced-camera-card-live> element first renders in
|
||||||
// background. These calls fail without waiting for loading here.
|
// the background. These calls fail without waiting for loading here.
|
||||||
protected _importPromises: Promise<unknown>[] = [];
|
protected _importPromises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -186,8 +185,6 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
} else if (provider === 'go2rtc') {
|
} else if (provider === 'go2rtc') {
|
||||||
this._importPromises.push(import('./providers/go2rtc/index.js'));
|
this._importPromises.push(import('./providers/go2rtc/index.js'));
|
||||||
}
|
}
|
||||||
|
|
||||||
this._dimensionsController.setCameraConfig(this.cameraConfig?.dimensions);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,29 +198,38 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _renderContainer(template: TemplateResult): TemplateResult {
|
protected _renderContainer(template: TemplateResult): TemplateResult {
|
||||||
// Place the zoomer in a separate div, as the zoom library misinterprets the
|
const intermediateTemplate = html` <advanced-camera-card-media-dimensions-container
|
||||||
// explicit width/height setting from the provider resizer as zooming.
|
.dimensionsConfig=${this.cameraConfig?.dimensions}
|
||||||
return html`<div class="container" ${ref(this._refContainer)}>
|
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||||
${this.liveConfig?.zoomable
|
if (ev.detail.placeholder) {
|
||||||
? html` <advanced-camera-card-zoomer
|
ev.stopPropagation();
|
||||||
.defaultSettings=${guard([this.cameraConfig?.dimensions?.layout], () =>
|
} else {
|
||||||
this.cameraConfig?.dimensions?.layout
|
this._videoMediaShowHandler();
|
||||||
? {
|
}
|
||||||
pan: this.cameraConfig.dimensions.layout.pan,
|
}}
|
||||||
zoom: this.cameraConfig.dimensions.layout.zoom,
|
>
|
||||||
}
|
${template}
|
||||||
: undefined,
|
</advanced-camera-card-media-dimensions-container>`;
|
||||||
)}
|
|
||||||
.settings=${this.zoomSettings}
|
return html` ${this.liveConfig?.zoomable
|
||||||
@advanced-camera-card:zoom:zoomed=${async () =>
|
? html` <advanced-camera-card-zoomer
|
||||||
(await this.getMediaPlayerController())?.setControls(false)}
|
.defaultSettings=${guard([this.cameraConfig?.dimensions?.layout], () =>
|
||||||
@advanced-camera-card:zoom:unzoomed=${async () =>
|
this.cameraConfig?.dimensions?.layout
|
||||||
(await this.getMediaPlayerController())?.setControls()}
|
? {
|
||||||
>
|
pan: this.cameraConfig.dimensions.layout.pan,
|
||||||
${template}
|
zoom: this.cameraConfig.dimensions.layout.zoom,
|
||||||
</advanced-camera-card-zoomer>`
|
}
|
||||||
: template}
|
: undefined,
|
||||||
</div>`;
|
)}
|
||||||
|
.settings=${this.zoomSettings}
|
||||||
|
@advanced-camera-card:zoom:zoomed=${async () =>
|
||||||
|
(await this.getMediaPlayerController())?.setControls(false)}
|
||||||
|
@advanced-camera-card:zoom:unzoomed=${async () =>
|
||||||
|
(await this.getMediaPlayerController())?.setControls()}
|
||||||
|
>
|
||||||
|
${intermediateTemplate}
|
||||||
|
</advanced-camera-card-zoomer>`
|
||||||
|
: intermediateTemplate}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
@@ -304,17 +310,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
})}
|
})}
|
||||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||||
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||||
if (provider === 'image') {
|
ev.detail.placeholder = provider !== 'image';
|
||||||
// Only count the media has loaded if the required provider is
|
|
||||||
// the image (not just the temporary image shown during
|
|
||||||
// loading).
|
|
||||||
this._videoMediaShowHandler();
|
|
||||||
} else {
|
|
||||||
// Manually call resize(), since the dimensions controller won't
|
|
||||||
// receive the after that stopPropagation().
|
|
||||||
this._dimensionsController.resize();
|
|
||||||
ev.stopPropagation();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
</advanced-camera-card-live-image>`
|
</advanced-camera-card-live-image>`
|
||||||
@@ -327,7 +323,6 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
?controls=${this.liveConfig.controls.builtin}
|
?controls=${this.liveConfig.controls.builtin}
|
||||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||||
@advanced-camera-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
|
|
||||||
>
|
>
|
||||||
</advanced-camera-card-live-ha>`
|
</advanced-camera-card-live-ha>`
|
||||||
: provider === 'go2rtc'
|
: provider === 'go2rtc'
|
||||||
@@ -341,9 +336,6 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
.microphoneConfig=${this.liveConfig.microphone}
|
.microphoneConfig=${this.liveConfig.microphone}
|
||||||
?controls=${this.liveConfig.controls.builtin}
|
?controls=${this.liveConfig.controls.builtin}
|
||||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||||
@advanced-camera-card:media:loaded=${this._videoMediaShowHandler.bind(
|
|
||||||
this,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
</advanced-camera-card-live-go2rtc>`
|
</advanced-camera-card-live-go2rtc>`
|
||||||
: provider === 'webrtc-card'
|
: provider === 'webrtc-card'
|
||||||
@@ -356,9 +348,6 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
?controls=${this.liveConfig.controls.builtin}
|
?controls=${this.liveConfig.controls.builtin}
|
||||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||||
@advanced-camera-card:media:loaded=${this._videoMediaShowHandler.bind(
|
|
||||||
this,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
</advanced-camera-card-live-webrtc-card>`
|
</advanced-camera-card-live-webrtc-card>`
|
||||||
: provider === 'jsmpeg'
|
: provider === 'jsmpeg'
|
||||||
@@ -370,9 +359,6 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
.cameraEndpoints=${this.cameraEndpoints}
|
.cameraEndpoints=${this.cameraEndpoints}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||||
@advanced-camera-card:media:loaded=${this._videoMediaShowHandler.bind(
|
|
||||||
this,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
</advanced-camera-card-live-jsmpeg>`
|
</advanced-camera-card-live-jsmpeg>`
|
||||||
: html``}
|
: html``}
|
||||||
@@ -402,10 +388,6 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
: ''}`;
|
: ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updated(): void {
|
|
||||||
this._dimensionsController.setContainer(this._refContainer.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(liveProviderStyle);
|
return unsafeCSS(liveProviderStyle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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 { MediaDimensionsContainerController } from '../components-lib/media-dimensions-container-controller.js';
|
||||||
|
import { CameraDimensionsConfig } from '../config/schema/cameras';
|
||||||
|
import mediaDimensionsContainerStyle from '../scss/media-dimensions-container.scss';
|
||||||
|
|
||||||
|
@customElement('advanced-camera-card-media-dimensions-container')
|
||||||
|
export class AdvancedCameraCardMediaDimensionsContainer extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public dimensionsConfig?: CameraDimensionsConfig;
|
||||||
|
|
||||||
|
protected _controller = new MediaDimensionsContainerController(this);
|
||||||
|
|
||||||
|
protected _refInnerContainer: Ref<HTMLElement> = createRef();
|
||||||
|
protected _refOuterContainer: Ref<HTMLElement> = createRef();
|
||||||
|
|
||||||
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
if (changedProps.has('dimensionsConfig')) {
|
||||||
|
this._controller.setConfig(this.dimensionsConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
return html`
|
||||||
|
<div class="outer" ${ref(this._refOuterContainer)}>
|
||||||
|
<div class="inner" ${ref(this._refInnerContainer)}>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(mediaDimensionsContainerStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public updated(): void {
|
||||||
|
this._controller.setContainers(
|
||||||
|
this._refInnerContainer.value,
|
||||||
|
this._refOuterContainer.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'advanced-camera-card-media-dimensions-container': AdvancedCameraCardMediaDimensionsContainer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
|||||||
import { CameraManager } from '../../camera-manager/manager.js';
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||||
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
||||||
import { MediaProviderDimensionsController } from '../../components-lib/media-provider-dimensions-controller.js';
|
|
||||||
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
||||||
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
||||||
import { CameraConfig } from '../../config/schema/cameras.js';
|
import { CameraConfig } from '../../config/schema/cameras.js';
|
||||||
@@ -38,6 +37,7 @@ import { QueryClassifier } from '../../view/query-classifier.js';
|
|||||||
import '../image-player.js';
|
import '../image-player.js';
|
||||||
import { renderProgressIndicator } from '../progress-indicator.js';
|
import { renderProgressIndicator } from '../progress-indicator.js';
|
||||||
import '../video-player.js';
|
import '../video-player.js';
|
||||||
|
import './../media-dimensions-container';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-viewer-provider')
|
@customElement('advanced-camera-card-viewer-provider')
|
||||||
export class AdvancedCameraCardViewerProvider extends LitElement implements MediaPlayer {
|
export class AdvancedCameraCardViewerProvider extends LitElement implements MediaPlayer {
|
||||||
@@ -65,7 +65,6 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||||
protected _refContainer: Ref<HTMLElement> = createRef();
|
protected _refContainer: Ref<HTMLElement> = createRef();
|
||||||
protected _lazyLoadController: LazyLoadController = new LazyLoadController(this);
|
protected _lazyLoadController: LazyLoadController = new LazyLoadController(this);
|
||||||
protected _dimensionsController = new MediaProviderDimensionsController(this);
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
protected _url: string | null = null;
|
protected _url: string | null = null;
|
||||||
@@ -201,12 +200,6 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
if (changedProps.has('viewerConfig') && this.viewerConfig?.zoomable) {
|
if (changedProps.has('viewerConfig') && this.viewerConfig?.zoomable) {
|
||||||
import('../zoomer.js');
|
import('../zoomer.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has('media') || changedProps.has('cameraManager')) {
|
|
||||||
this._dimensionsController.setCameraConfig(
|
|
||||||
this._getRelevantCameraConfig()?.dimensions,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getRelevantCameraConfig(): CameraConfig | null {
|
private _getRelevantCameraConfig(): CameraConfig | null {
|
||||||
@@ -227,9 +220,13 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
: null;
|
: null;
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
|
|
||||||
// Place the zoomer in a separate div, as the zoom library misinterprets the
|
const intermediateTemplate = html` <advanced-camera-card-media-dimensions-container
|
||||||
// explicit width/height setting from the provider resizer as zooming.
|
.dimensionsConfig=${this._getRelevantCameraConfig()?.dimensions}
|
||||||
return html`<div class="container" ${ref(this._refContainer)}>
|
>
|
||||||
|
${template}
|
||||||
|
</advanced-camera-card-media-dimensions-container>`;
|
||||||
|
|
||||||
|
return html`
|
||||||
${this.viewerConfig?.zoomable
|
${this.viewerConfig?.zoomable
|
||||||
? html`<advanced-camera-card-zoomer
|
? html`<advanced-camera-card-zoomer
|
||||||
.defaultSettings=${guard([cameraConfig?.dimensions?.layout], () =>
|
.defaultSettings=${guard([cameraConfig?.dimensions?.layout], () =>
|
||||||
@@ -254,10 +251,10 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
mediaID,
|
mediaID,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
${template}
|
${intermediateTemplate}
|
||||||
</advanced-camera-card-zoomer>`
|
</advanced-camera-card-zoomer>`
|
||||||
: template}
|
: intermediateTemplate}
|
||||||
</div>`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
@@ -319,10 +316,6 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updated(): void {
|
|
||||||
this._dimensionsController.setContainer(this._refContainer.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(viewerProviderStyle);
|
return unsafeCSS(viewerProviderStyle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,9 +146,17 @@ const proxyConfigSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type ProxyConfig = z.infer<typeof proxyConfigSchema>;
|
export type ProxyConfig = z.infer<typeof proxyConfigSchema>;
|
||||||
|
|
||||||
|
const rotationSchema = z
|
||||||
|
.literal(0)
|
||||||
|
.or(z.literal(90))
|
||||||
|
.or(z.literal(180))
|
||||||
|
.or(z.literal(270));
|
||||||
|
export type Rotation = z.infer<typeof rotationSchema>;
|
||||||
|
|
||||||
const cameraDimensionsSchema = z.object({
|
const cameraDimensionsSchema = z.object({
|
||||||
aspect_ratio: aspectRatioSchema.optional(),
|
aspect_ratio: aspectRatioSchema.optional(),
|
||||||
layout: mediaLayoutConfigSchema.optional(),
|
layout: mediaLayoutConfigSchema.optional(),
|
||||||
|
rotation: rotationSchema.optional(),
|
||||||
});
|
});
|
||||||
export type CameraDimensionsConfig = z.infer<typeof cameraDimensionsSchema>;
|
export type CameraDimensionsConfig = z.infer<typeof cameraDimensionsSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export const CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH =
|
|||||||
`${CONF_CAMERAS}.#.cast.dashboard.view_path` as const;
|
`${CONF_CAMERAS}.#.cast.dashboard.view_path` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_DIMENSIONS_ASPECT_RATIO =
|
export const CONF_CAMERAS_ARRAY_DIMENSIONS_ASPECT_RATIO =
|
||||||
`${CONF_CAMERAS}.#.dimensions.aspect_ratio` as const;
|
`${CONF_CAMERAS}.#.dimensions.aspect_ratio` as const;
|
||||||
|
export const CONF_CAMERAS_ARRAY_DIMENSIONS_ROTATION =
|
||||||
|
`${CONF_CAMERAS}.#.dimensions.rotation` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID =
|
export const CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID =
|
||||||
`${CONF_CAMERAS}.#.frigate.client_id` as const;
|
`${CONF_CAMERAS}.#.frigate.client_id` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_FRIGATE_LABELS =
|
export const CONF_CAMERAS_ARRAY_FRIGATE_LABELS =
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_RIGHT,
|
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_RIGHT,
|
||||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_TOP,
|
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_TOP,
|
||||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_ZOOM_FACTOR,
|
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_ZOOM_FACTOR,
|
||||||
|
CONF_CAMERAS_ARRAY_DIMENSIONS_ROTATION,
|
||||||
CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME,
|
CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME,
|
||||||
CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID,
|
CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID,
|
||||||
CONF_CAMERAS_ARRAY_FRIGATE_LABELS,
|
CONF_CAMERAS_ARRAY_FRIGATE_LABELS,
|
||||||
@@ -962,6 +963,14 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
{ value: 'traditional', label: localize('config.view.theme.themes.traditional') },
|
{ value: 'traditional', label: localize('config.view.theme.themes.traditional') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected _rotations: EditorSelectOption[] = [
|
||||||
|
{ value: '', label: '' },
|
||||||
|
{ value: 0, label: localize('config.cameras.dimensions.rotations.0') },
|
||||||
|
{ value: 90, label: localize('config.cameras.dimensions.rotations.90') },
|
||||||
|
{ value: 180, label: localize('config.cameras.dimensions.rotations.180') },
|
||||||
|
{ value: 270, label: localize('config.cameras.dimensions.rotations.270') },
|
||||||
|
];
|
||||||
|
|
||||||
public setConfig(config: RawAdvancedCameraCardConfig): void {
|
public setConfig(config: RawAdvancedCameraCardConfig): void {
|
||||||
// Note: This does not use Zod to parse the full configuration, so it may be
|
// Note: This does not use Zod to parse the full configuration, so it may be
|
||||||
// partially or completely invalid. It's more useful to have a partially
|
// partially or completely invalid. It's more useful to have a partially
|
||||||
@@ -2466,6 +2475,13 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
cameraIndex,
|
cameraIndex,
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
${this._renderOptionSelector(
|
||||||
|
getArrayConfigPath(
|
||||||
|
CONF_CAMERAS_ARRAY_DIMENSIONS_ROTATION,
|
||||||
|
cameraIndex,
|
||||||
|
),
|
||||||
|
this._rotations,
|
||||||
|
)}
|
||||||
${this._renderMediaLayout(
|
${this._renderMediaLayout(
|
||||||
MENU_CAMERAS_DIMENSIONS_LAYOUT,
|
MENU_CAMERAS_DIMENSIONS_LAYOUT,
|
||||||
'config.cameras.dimensions.layout.editor_label',
|
'config.cameras.dimensions.layout.editor_label',
|
||||||
|
|||||||
@@ -79,6 +79,13 @@
|
|||||||
"top": ""
|
"top": ""
|
||||||
},
|
},
|
||||||
"zoom": ""
|
"zoom": ""
|
||||||
|
},
|
||||||
|
"rotation": "",
|
||||||
|
"rotations": {
|
||||||
|
"0": "",
|
||||||
|
"90": "",
|
||||||
|
"180": "",
|
||||||
|
"270": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -79,6 +79,13 @@
|
|||||||
"top": "Top inset percentage"
|
"top": "Top inset percentage"
|
||||||
},
|
},
|
||||||
"zoom": "Zoom factor"
|
"zoom": "Zoom factor"
|
||||||
|
},
|
||||||
|
"rotation": "Rotation",
|
||||||
|
"rotations": {
|
||||||
|
"0": "No rotation",
|
||||||
|
"90": "90 degrees clockwise",
|
||||||
|
"180": "180 degrees clockwise",
|
||||||
|
"270": "270 degrees clockwise"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -79,6 +79,13 @@
|
|||||||
"top": "Pourcentage d'écart en haut"
|
"top": "Pourcentage d'écart en haut"
|
||||||
},
|
},
|
||||||
"zoom": "Facteur de zoom"
|
"zoom": "Facteur de zoom"
|
||||||
|
},
|
||||||
|
"rotation": "",
|
||||||
|
"rotations": {
|
||||||
|
"0": "",
|
||||||
|
"90": "",
|
||||||
|
"180": "",
|
||||||
|
"270": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -79,6 +79,13 @@
|
|||||||
"top": ""
|
"top": ""
|
||||||
},
|
},
|
||||||
"zoom": ""
|
"zoom": ""
|
||||||
|
},
|
||||||
|
"rotation": "",
|
||||||
|
"rotations": {
|
||||||
|
"0": "",
|
||||||
|
"90": "",
|
||||||
|
"180": "",
|
||||||
|
"270": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -79,6 +79,13 @@
|
|||||||
"top": ""
|
"top": ""
|
||||||
},
|
},
|
||||||
"zoom": ""
|
"zoom": ""
|
||||||
|
},
|
||||||
|
"rotation": "",
|
||||||
|
"rotations": {
|
||||||
|
"0": "",
|
||||||
|
"90": "",
|
||||||
|
"180": "",
|
||||||
|
"270": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -79,6 +79,13 @@
|
|||||||
"top": ""
|
"top": ""
|
||||||
},
|
},
|
||||||
"zoom": ""
|
"zoom": ""
|
||||||
|
},
|
||||||
|
"rotation": "",
|
||||||
|
"rotations": {
|
||||||
|
"0": "",
|
||||||
|
"90": "",
|
||||||
|
"180": "",
|
||||||
|
"270": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
:host,
|
||||||
|
.outer {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
// The container will be sized by the provider resize controller, but don't
|
||||||
|
// allow the outer container to shrink or contain the inner (as we need to
|
||||||
|
// calculate the true intrinsic size, otherwise on landscape videos the
|
||||||
|
// rotated size will be incorrectly calculated).
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
:host([rotated]) .inner {
|
||||||
|
transform: rotate(var(--advanced-camera-card-media-rotation, 0deg));
|
||||||
|
}
|
||||||
+2
-26
@@ -1,32 +1,8 @@
|
|||||||
@use 'media-background.scss';
|
@use 'media-background.scss';
|
||||||
|
@use 'basic-block.scss';
|
||||||
|
|
||||||
:host {
|
.zoom-wrapper {
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
// The container will be sized by the provider resize controller.
|
|
||||||
width: fit-content;
|
|
||||||
height: auto;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([size='max']) > .container {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
:host([size='max-height']) > .container {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
:host([size='max-width']) > .container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ export interface MediaLoadedInfo {
|
|||||||
|
|
||||||
mediaPlayerController?: MediaPlayerController;
|
mediaPlayerController?: MediaPlayerController;
|
||||||
capabilities?: MediaLoadedCapabilities;
|
capabilities?: MediaLoadedCapabilities;
|
||||||
|
|
||||||
|
// Whether or not this media is a placeholder (temporary image) whilst another
|
||||||
|
// media item is being loaded.
|
||||||
|
placeholder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageType = 'info' | 'error' | 'connection' | 'diagnostics';
|
export type MessageType = 'info' | 'error' | 'connection' | 'diagnostics';
|
||||||
|
|||||||
+22
-1
@@ -209,6 +209,27 @@ export const setOrRemoveAttribute = <T extends string>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set or remove a style property on a HTMLElement.
|
||||||
|
* @param element The element.
|
||||||
|
* @param set If `true` sets the property, otherwise removes it.
|
||||||
|
* @param name The property name.
|
||||||
|
* @param value An optional value to set the property to. Never used if set is
|
||||||
|
* `false`.
|
||||||
|
*/
|
||||||
|
export const setOrRemoveStyleProperty = <T extends string>(
|
||||||
|
element: HTMLElement,
|
||||||
|
set: boolean,
|
||||||
|
name: string,
|
||||||
|
value?: T,
|
||||||
|
): void => {
|
||||||
|
if (set) {
|
||||||
|
element.style.setProperty(name, value ?? '');
|
||||||
|
} else {
|
||||||
|
element.style.removeProperty(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow typescript to narrow types based on truthy filter.
|
* Allow typescript to narrow types based on truthy filter.
|
||||||
*/
|
*/
|
||||||
@@ -243,7 +264,7 @@ export const recursivelyMergeObjectsConcatenatingArraysUniquely = <T>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const aspectRatioToString = (options?: {
|
export const aspectRatioToString = (options?: {
|
||||||
ratio?: number[];
|
ratio?: number[] | null;
|
||||||
defaultStatic?: boolean;
|
defaultStatic?: boolean;
|
||||||
}): string => {
|
}): string => {
|
||||||
if (options?.ratio && options.ratio.length === 2) {
|
if (options?.ratio && options.ratio.length === 2) {
|
||||||
|
|||||||
+21
-28
@@ -1,4 +1,5 @@
|
|||||||
import { MediaLayoutConfig } from '../config/schema/camera/media-layout';
|
import { MediaLayoutConfig } from '../config/schema/camera/media-layout';
|
||||||
|
import { setOrRemoveStyleProperty } from './basic';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update element style from a media configuration.
|
* Update element style from a media configuration.
|
||||||
@@ -9,36 +10,28 @@ export const updateElementStyleFromMediaLayoutConfig = (
|
|||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
mediaLayoutConfig?: MediaLayoutConfig,
|
mediaLayoutConfig?: MediaLayoutConfig,
|
||||||
): void => {
|
): void => {
|
||||||
if (mediaLayoutConfig?.fit !== undefined) {
|
setOrRemoveStyleProperty(
|
||||||
element.style.setProperty(
|
element,
|
||||||
'--advanced-camera-card-media-layout-fit',
|
!!mediaLayoutConfig?.fit,
|
||||||
mediaLayoutConfig.fit,
|
'--advanced-camera-card-media-layout-fit',
|
||||||
);
|
mediaLayoutConfig?.fit,
|
||||||
} else {
|
);
|
||||||
element.style.removeProperty('--advanced-camera-card-media-layout-fit');
|
|
||||||
}
|
|
||||||
for (const dimension of ['x', 'y']) {
|
for (const dimension of ['x', 'y']) {
|
||||||
if (mediaLayoutConfig?.position?.[dimension] !== undefined) {
|
setOrRemoveStyleProperty(
|
||||||
element.style.setProperty(
|
element,
|
||||||
`--advanced-camera-card-media-layout-position-${dimension}`,
|
!!mediaLayoutConfig?.position?.[dimension],
|
||||||
`${mediaLayoutConfig.position[dimension]}%`,
|
`--advanced-camera-card-media-layout-position-${dimension}`,
|
||||||
);
|
`${mediaLayoutConfig?.position?.[dimension]}%`,
|
||||||
} else {
|
);
|
||||||
element.style.removeProperty(
|
|
||||||
`--advanced-camera-card-media-layout-position-${dimension}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const dimension of ['top', 'bottom', 'left', 'right']) {
|
for (const dimension of ['top', 'bottom', 'left', 'right']) {
|
||||||
if (mediaLayoutConfig?.view_box?.[dimension] !== undefined) {
|
setOrRemoveStyleProperty(
|
||||||
element.style.setProperty(
|
element,
|
||||||
`--advanced-camera-card-media-layout-view-box-${dimension}`,
|
!!mediaLayoutConfig?.view_box?.[dimension],
|
||||||
`${mediaLayoutConfig.view_box[dimension]}%`,
|
`--advanced-camera-card-media-layout-view-box-${dimension}`,
|
||||||
);
|
`${mediaLayoutConfig?.view_box?.[dimension]}%`,
|
||||||
} else {
|
);
|
||||||
element.style.removeProperty(
|
|
||||||
`--advanced-camera-card-media-layout-view-box-${dimension}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,733 @@
|
|||||||
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { MediaDimensionsContainerController } from '../../src/components-lib/media-dimensions-container-controller';
|
||||||
|
import { CameraDimensionsConfig, Rotation } from '../../src/config/schema/cameras';
|
||||||
|
import { MediaLoadedInfo } from '../../src/types';
|
||||||
|
import {
|
||||||
|
callResizeHandler,
|
||||||
|
createLitElement,
|
||||||
|
getResizeObserver,
|
||||||
|
ResizeObserverMock,
|
||||||
|
} from '../test-utils';
|
||||||
|
|
||||||
|
vi.mock('lodash-es', () => ({
|
||||||
|
debounce: vi.fn((fn) => fn),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('MediaDimensionsContainerController', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const configWithAspectRatio: CameraDimensionsConfig = {
|
||||||
|
aspect_ratio: [16, 9],
|
||||||
|
};
|
||||||
|
const configWithRotation: CameraDimensionsConfig = {
|
||||||
|
rotation: 90,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should construct', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const eventListener = vi.fn();
|
||||||
|
host.addEventListener = eventListener;
|
||||||
|
|
||||||
|
new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
const observer = getResizeObserver();
|
||||||
|
|
||||||
|
// No resize observer should be created.
|
||||||
|
expect(observer?.observe).not.toBeCalled();
|
||||||
|
expect(eventListener).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should connect and disconnect', () => {
|
||||||
|
it('should connect and disconnect without a container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
const observer = getResizeObserver();
|
||||||
|
expect(observer?.observe).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
controller.hostConnected();
|
||||||
|
expect(observer?.observe).toBeCalledWith(host);
|
||||||
|
expect(observer?.observe).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
controller.hostDisconnected();
|
||||||
|
expect(observer?.disconnect).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should connect and disconnect with a container when host is connected', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
Object.defineProperty(host, 'isConnected', {
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
const observer = getResizeObserver(0);
|
||||||
|
|
||||||
|
const container = createLitElement();
|
||||||
|
controller.setContainers(container);
|
||||||
|
|
||||||
|
expect(observer?.observe).not.toBeCalled();
|
||||||
|
|
||||||
|
controller.hostDisconnected();
|
||||||
|
expect(observer?.disconnect).toBeCalled();
|
||||||
|
|
||||||
|
controller.hostConnected();
|
||||||
|
expect(observer?.observe).toBeCalledWith(host);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should set container respecting config', () => {
|
||||||
|
it('should set aspect ratio on container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
controller.setConfig(configWithAspectRatio);
|
||||||
|
controller.setContainers(container);
|
||||||
|
|
||||||
|
expect(container.style.aspectRatio).toBe('16 / 9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set layout attributes', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
layout: {
|
||||||
|
fit: 'contain' as const,
|
||||||
|
position: { x: 1, y: 2 },
|
||||||
|
view_box: { top: 3, bottom: 4, left: 5, right: 6 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
controller.setConfig(config);
|
||||||
|
controller.setContainers(container);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
container.style.getPropertyValue('--advanced-camera-card-media-layout-fit'),
|
||||||
|
).toBe('contain');
|
||||||
|
expect(
|
||||||
|
container.style.getPropertyValue(
|
||||||
|
'--advanced-camera-card-media-layout-position-x',
|
||||||
|
),
|
||||||
|
).toBe('1%');
|
||||||
|
expect(
|
||||||
|
container.style.getPropertyValue(
|
||||||
|
'--advanced-camera-card-media-layout-position-y',
|
||||||
|
),
|
||||||
|
).toBe('2%');
|
||||||
|
expect(
|
||||||
|
container.style.getPropertyValue(
|
||||||
|
'--advanced-camera-card-media-layout-view-box-top',
|
||||||
|
),
|
||||||
|
).toBe('3%');
|
||||||
|
expect(
|
||||||
|
container.style.getPropertyValue(
|
||||||
|
'--advanced-camera-card-media-layout-view-box-bottom',
|
||||||
|
),
|
||||||
|
).toBe('4%');
|
||||||
|
expect(
|
||||||
|
container.style.getPropertyValue(
|
||||||
|
'--advanced-camera-card-media-layout-view-box-left',
|
||||||
|
),
|
||||||
|
).toBe('5%');
|
||||||
|
expect(
|
||||||
|
container.style.getPropertyValue(
|
||||||
|
'--advanced-camera-card-media-layout-view-box-right',
|
||||||
|
),
|
||||||
|
).toBe('6%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore multiple calls to set same container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
controller.setConfig(configWithAspectRatio);
|
||||||
|
|
||||||
|
controller.setContainers(container);
|
||||||
|
expect(container.style.aspectRatio).toBe('16 / 9');
|
||||||
|
|
||||||
|
container.style.aspectRatio = '';
|
||||||
|
|
||||||
|
controller.setContainers(container);
|
||||||
|
expect(container.style.aspectRatio).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore multiple calls to set same config', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
controller.setConfig(configWithAspectRatio);
|
||||||
|
|
||||||
|
controller.setContainers(container);
|
||||||
|
|
||||||
|
container.style.aspectRatio = '';
|
||||||
|
|
||||||
|
controller.setConfig(configWithAspectRatio);
|
||||||
|
|
||||||
|
expect(container.style.aspectRatio).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset config', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
controller.setConfig(configWithAspectRatio);
|
||||||
|
|
||||||
|
controller.setContainers(container);
|
||||||
|
|
||||||
|
expect(container.style.aspectRatio).toBe('16 / 9');
|
||||||
|
|
||||||
|
controller.setConfig();
|
||||||
|
|
||||||
|
expect(container.style.aspectRatio).toBe('auto');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should respond to size changes', () => {
|
||||||
|
it('should ignore without an aspect ratio or rotation', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 600,
|
||||||
|
width: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
const outerContainer = document.createElement('div');
|
||||||
|
innerContainer.style.width = '1px';
|
||||||
|
innerContainer.style.height = '2px';
|
||||||
|
outerContainer.style.width = '3px';
|
||||||
|
outerContainer.style.height = '4px';
|
||||||
|
controller.setContainers(innerContainer, outerContainer);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(innerContainer.style.width).toBe('100%');
|
||||||
|
expect(innerContainer.style.height).toBe('100%');
|
||||||
|
expect(outerContainer.style.width).toBe('100%');
|
||||||
|
expect(outerContainer.style.height).toBe('100%');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should ignore without containers', () => {
|
||||||
|
describe.each([
|
||||||
|
['with aspect ratio', configWithAspectRatio],
|
||||||
|
['with rotation', configWithRotation],
|
||||||
|
])('%s', async (_name: string, config: CameraDimensionsConfig) => {
|
||||||
|
it('should ignore without inner container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
controller.setConfig(config);
|
||||||
|
|
||||||
|
const outerContainer = document.createElement('div');
|
||||||
|
outerContainer.style.width = '1px';
|
||||||
|
outerContainer.style.height = '2px';
|
||||||
|
|
||||||
|
controller.setContainers(undefined, outerContainer);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(outerContainer.style.width).toBe('1px');
|
||||||
|
expect(outerContainer.style.height).toBe('2px');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore without outer container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
controller.setConfig(config);
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.style.width = '1px';
|
||||||
|
innerContainer.style.height = '2px';
|
||||||
|
|
||||||
|
controller.setContainers(innerContainer);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(innerContainer.style.width).toBe('1px');
|
||||||
|
expect(innerContainer.style.height).toBe('2px');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should ignore without container sizes', () => {
|
||||||
|
describe('should ignore without container sizes with aspect ratio', () => {
|
||||||
|
it('should ignore without inner container size', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
controller.setConfig(configWithAspectRatio);
|
||||||
|
|
||||||
|
const outerContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
innerContainer.style.width = '1px';
|
||||||
|
innerContainer.style.height = '2px';
|
||||||
|
|
||||||
|
controller.setContainers(innerContainer, outerContainer);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(outerContainer.style.width).toBe('100%');
|
||||||
|
expect(outerContainer.style.height).toBe('100%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore without host size', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
controller.setConfig(configWithAspectRatio);
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.style.width = '100px';
|
||||||
|
innerContainer.style.height = '200px';
|
||||||
|
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.setContainers(innerContainer, document.createElement('div'));
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(innerContainer.style.width).toBe('100%');
|
||||||
|
expect(innerContainer.style.height).toBe('100%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should ignore without container sizes with rotation', () => {
|
||||||
|
it('should ignore without inner container size', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
controller.setConfig(configWithRotation);
|
||||||
|
|
||||||
|
const outerContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
innerContainer.style.width = '1px';
|
||||||
|
innerContainer.style.height = '2px';
|
||||||
|
|
||||||
|
controller.setContainers(innerContainer, outerContainer);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(outerContainer.style.width).toBe('100%');
|
||||||
|
expect(outerContainer.style.height).toBe('100%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore without host size', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
controller.setConfig(configWithRotation);
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.style.width = '100px';
|
||||||
|
innerContainer.style.height = '200px';
|
||||||
|
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.setContainers(innerContainer, document.createElement('div'));
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(innerContainer.style.width).toBe('100%');
|
||||||
|
expect(innerContainer.style.height).toBe('100%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore without inner container size during resize', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
controller.setConfig(configWithRotation);
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.style.width = '100px';
|
||||||
|
innerContainer.style.height = '200px';
|
||||||
|
innerContainer.getBoundingClientRect = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
height: 100,
|
||||||
|
width: 200,
|
||||||
|
})
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.setContainers(innerContainer, document.createElement('div'));
|
||||||
|
|
||||||
|
expect(innerContainer.style.width).toBe('100%');
|
||||||
|
expect(innerContainer.style.height).toBe('100%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore without host size during resize', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
height: 100,
|
||||||
|
width: 200,
|
||||||
|
})
|
||||||
|
.mockReturnValueOnce({
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
controller.setConfig(configWithRotation);
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.style.width = '100px';
|
||||||
|
innerContainer.style.height = '200px';
|
||||||
|
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.setContainers(innerContainer, document.createElement('div'));
|
||||||
|
|
||||||
|
expect(innerContainer.style.width).toBe('100%');
|
||||||
|
expect(innerContainer.style.height).toBe('100%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should set rotation attributes and properties on container', () => {
|
||||||
|
it.each([
|
||||||
|
[false, undefined],
|
||||||
|
[false, 0 as const],
|
||||||
|
[true, 90 as const],
|
||||||
|
[true, 180 as const],
|
||||||
|
[true, 270 as const],
|
||||||
|
])(
|
||||||
|
'should rotate %s with rotation degrees %s',
|
||||||
|
(shouldRotate: boolean, degrees?: Rotation) => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
controller.setConfig({
|
||||||
|
rotation: degrees,
|
||||||
|
});
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
controller.setContainers(innerContainer, document.createElement('div'));
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(host.hasAttribute('rotated')).toBe(shouldRotate);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
innerContainer.style.getPropertyValue(
|
||||||
|
'--advanced-camera-card-media-rotation',
|
||||||
|
),
|
||||||
|
).toBe(shouldRotate ? `${degrees}deg` : '');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should resize container with aspect-ratio', () => {
|
||||||
|
it('should resize container to fit width-limited container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 200,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 90,
|
||||||
|
width: 160,
|
||||||
|
});
|
||||||
|
const outerContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
controller.setConfig(configWithAspectRatio);
|
||||||
|
controller.setContainers(innerContainer, outerContainer);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(innerContainer.style.width).toBe('100%');
|
||||||
|
expect(innerContainer.style.height).toBe('auto');
|
||||||
|
expect(outerContainer.style.width).toBe('100%');
|
||||||
|
expect(outerContainer.style.height).toBe('100%');
|
||||||
|
|
||||||
|
expect(host.hasAttribute('rotated')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resize container to fit height-limited container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 400,
|
||||||
|
});
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 200,
|
||||||
|
width: 160,
|
||||||
|
});
|
||||||
|
const outerContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
controller.setConfig(configWithAspectRatio);
|
||||||
|
controller.setContainers(innerContainer, outerContainer);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(innerContainer.style.width).toBe(`auto`);
|
||||||
|
expect(innerContainer.style.height).toBe('100%');
|
||||||
|
expect(outerContainer.style.width).toBe('100%');
|
||||||
|
expect(outerContainer.style.height).toBe('100%');
|
||||||
|
|
||||||
|
expect(host.hasAttribute('rotated')).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should resize container with rotation', () => {
|
||||||
|
it('should resize container to fit width-limited container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
width: 500,
|
||||||
|
height: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.getBoundingClientRect = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({
|
||||||
|
// These are the pre-rotation dimensions.
|
||||||
|
width: 1000,
|
||||||
|
height: 2000,
|
||||||
|
})
|
||||||
|
.mockReturnValue({
|
||||||
|
// These are the post-rotation dimensions.
|
||||||
|
width: 500,
|
||||||
|
height: 1000,
|
||||||
|
});
|
||||||
|
const outerContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
controller.setConfig(configWithRotation);
|
||||||
|
controller.setContainers(innerContainer, outerContainer);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
// These are pre-rotation dimensions.
|
||||||
|
expect(innerContainer.style.width).toBe('250px');
|
||||||
|
expect(innerContainer.style.height).toBe('500px');
|
||||||
|
|
||||||
|
expect(outerContainer.style.width).toBe('100%');
|
||||||
|
expect(outerContainer.style.height).toBe('250px');
|
||||||
|
|
||||||
|
expect(host.hasAttribute('rotated')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resize container to fit height-limited container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
width: 500,
|
||||||
|
height: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.getBoundingClientRect = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({
|
||||||
|
// These are the pre-rotation dimensions.
|
||||||
|
width: 1000,
|
||||||
|
height: 2000,
|
||||||
|
})
|
||||||
|
.mockReturnValue({
|
||||||
|
// These are the post-rotation dimensions.
|
||||||
|
width: 500,
|
||||||
|
height: 2000,
|
||||||
|
});
|
||||||
|
const outerContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
controller.setConfig(configWithRotation);
|
||||||
|
controller.setContainers(innerContainer, outerContainer);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
// These are pre-rotation dimensions.
|
||||||
|
expect(innerContainer.style.width).toBe('1000px');
|
||||||
|
expect(innerContainer.style.height).toBe('4000px');
|
||||||
|
|
||||||
|
expect(outerContainer.style.width).toBe('100%');
|
||||||
|
expect(outerContainer.style.height).toBe('1000px');
|
||||||
|
|
||||||
|
expect(host.hasAttribute('rotated')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should respond to slot changes', () => {
|
||||||
|
it('should resize container on slotchange event', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 200,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 90,
|
||||||
|
width: 160,
|
||||||
|
});
|
||||||
|
const outerContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
Object.defineProperty(host, 'isConnected', {
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
controller.hostConnected();
|
||||||
|
|
||||||
|
controller.setConfig(configWithRotation);
|
||||||
|
controller.setContainers(innerContainer, outerContainer);
|
||||||
|
|
||||||
|
host.removeAttribute('rotated');
|
||||||
|
|
||||||
|
innerContainer.dispatchEvent(new Event('slotchange'));
|
||||||
|
|
||||||
|
expect(host.hasAttribute('rotated')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should respond to media load', () => {
|
||||||
|
it('should resize container on media load', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 200,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 90,
|
||||||
|
width: 160,
|
||||||
|
});
|
||||||
|
const outerContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
Object.defineProperty(host, 'isConnected', {
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
controller.hostConnected();
|
||||||
|
|
||||||
|
controller.setConfig(configWithRotation);
|
||||||
|
controller.setContainers(innerContainer, outerContainer);
|
||||||
|
|
||||||
|
host.removeAttribute('rotated');
|
||||||
|
|
||||||
|
const mediaLoadedInfo: MediaLoadedInfo = {
|
||||||
|
width: 90,
|
||||||
|
height: 160,
|
||||||
|
};
|
||||||
|
innerContainer.dispatchEvent(
|
||||||
|
new CustomEvent<MediaLoadedInfo>('advanced-camera-card:media:loaded', {
|
||||||
|
detail: mediaLoadedInfo,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(host.hasAttribute('rotated')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not resize container on media load to same size', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 200,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 90,
|
||||||
|
width: 160,
|
||||||
|
});
|
||||||
|
const outerContainer = document.createElement('div');
|
||||||
|
|
||||||
|
const controller = new MediaDimensionsContainerController(host);
|
||||||
|
|
||||||
|
Object.defineProperty(host, 'isConnected', {
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
controller.hostConnected();
|
||||||
|
|
||||||
|
controller.setConfig(configWithRotation);
|
||||||
|
controller.setContainers(innerContainer, outerContainer);
|
||||||
|
|
||||||
|
const mediaLoadedInfo: MediaLoadedInfo = {
|
||||||
|
width: 90,
|
||||||
|
height: 160,
|
||||||
|
};
|
||||||
|
innerContainer.dispatchEvent(
|
||||||
|
new CustomEvent<MediaLoadedInfo>('advanced-camera-card:media:loaded', {
|
||||||
|
detail: mediaLoadedInfo,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(host.hasAttribute('rotated')).toBeTruthy();
|
||||||
|
host.removeAttribute('rotated');
|
||||||
|
|
||||||
|
innerContainer.dispatchEvent(
|
||||||
|
new CustomEvent<MediaLoadedInfo>('advanced-camera-card:media:loaded', {
|
||||||
|
detail: mediaLoadedInfo,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(host.hasAttribute('rotated')).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
import {
|
|
||||||
afterAll,
|
|
||||||
afterEach,
|
|
||||||
beforeAll,
|
|
||||||
beforeEach,
|
|
||||||
describe,
|
|
||||||
expect,
|
|
||||||
it,
|
|
||||||
vi,
|
|
||||||
} from 'vitest';
|
|
||||||
import { MediaProviderDimensionsController } from '../../src/components-lib/media-provider-dimensions-controller';
|
|
||||||
import { CameraDimensionsConfig } from '../../src/config/schema/cameras';
|
|
||||||
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../src/utils/media-info';
|
|
||||||
import {
|
|
||||||
callResizeHandler,
|
|
||||||
createLitElement,
|
|
||||||
createMediaLoadedInfo,
|
|
||||||
getResizeObserver,
|
|
||||||
requestAnimationFrameMock,
|
|
||||||
ResizeObserverMock,
|
|
||||||
} from '../test-utils';
|
|
||||||
|
|
||||||
vi.mock('lodash-es', () => ({
|
|
||||||
throttle: vi.fn((fn) => fn),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('MediaProviderDimensionsController', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
|
||||||
});
|
|
||||||
afterAll(() => {
|
|
||||||
vi.unstubAllGlobals();
|
|
||||||
});
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const configWithAspectRatioLandscape: CameraDimensionsConfig = {
|
|
||||||
aspect_ratio: [16, 9],
|
|
||||||
};
|
|
||||||
const configWithAspectRatioPortrait: CameraDimensionsConfig = {
|
|
||||||
aspect_ratio: [9, 16],
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should construct', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const eventListener = vi.fn();
|
|
||||||
host.addEventListener = eventListener;
|
|
||||||
|
|
||||||
new MediaProviderDimensionsController(host);
|
|
||||||
|
|
||||||
const observer = getResizeObserver();
|
|
||||||
|
|
||||||
// No resize observer should be created.
|
|
||||||
expect(observer?.observe).not.toBeCalled();
|
|
||||||
expect(eventListener).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should connect and disconnect', () => {
|
|
||||||
it('should connect and disconnect without a container', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
|
|
||||||
const observer = getResizeObserver();
|
|
||||||
expect(observer?.observe).toBeCalledTimes(0);
|
|
||||||
|
|
||||||
controller.hostConnected();
|
|
||||||
expect(observer?.observe).toBeCalledWith(host);
|
|
||||||
expect(observer?.observe).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
controller.hostDisconnected();
|
|
||||||
expect(observer?.disconnect).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should connect and disconnect with a container when host is connected', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
Object.defineProperty(host, 'isConnected', {
|
|
||||||
value: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
|
|
||||||
const observer = getResizeObserver(0);
|
|
||||||
|
|
||||||
const container = createLitElement();
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
expect(observer?.observe).not.toBeCalled();
|
|
||||||
|
|
||||||
controller.hostDisconnected();
|
|
||||||
expect(observer?.disconnect).toBeCalled();
|
|
||||||
|
|
||||||
controller.hostConnected();
|
|
||||||
expect(observer?.observe).toBeCalledWith(host);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should set container respecting config ', () => {
|
|
||||||
it('should set aspect ratio on container', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
|
|
||||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
expect(container.style.aspectRatio).toBe('16 / 9');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should set host attribute', () => {
|
|
||||||
it.each([
|
|
||||||
['max', {}],
|
|
||||||
['max-width', { aspect_ratio: [16, 9] }],
|
|
||||||
['max-height', { aspect_ratio: [9, 16] }],
|
|
||||||
])('%s', async (value: string, config: CameraDimensionsConfig) => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
controller.setCameraConfig(config);
|
|
||||||
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
expect(host.getAttribute('size')).toBe(value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set layout attributes', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
layout: {
|
|
||||||
fit: 'contain' as const,
|
|
||||||
position: { x: 1, y: 2 },
|
|
||||||
view_box: { top: 3, bottom: 4, left: 5, right: 6 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
controller.setCameraConfig(config);
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
host.style.getPropertyValue('--advanced-camera-card-media-layout-fit'),
|
|
||||||
).toBe('contain');
|
|
||||||
expect(
|
|
||||||
host.style.getPropertyValue('--advanced-camera-card-media-layout-position-x'),
|
|
||||||
).toBe('1%');
|
|
||||||
expect(
|
|
||||||
host.style.getPropertyValue('--advanced-camera-card-media-layout-position-y'),
|
|
||||||
).toBe('2%');
|
|
||||||
expect(
|
|
||||||
host.style.getPropertyValue('--advanced-camera-card-media-layout-view-box-top'),
|
|
||||||
).toBe('3%');
|
|
||||||
expect(
|
|
||||||
host.style.getPropertyValue(
|
|
||||||
'--advanced-camera-card-media-layout-view-box-bottom',
|
|
||||||
),
|
|
||||||
).toBe('4%');
|
|
||||||
expect(
|
|
||||||
host.style.getPropertyValue('--advanced-camera-card-media-layout-view-box-left'),
|
|
||||||
).toBe('5%');
|
|
||||||
expect(
|
|
||||||
host.style.getPropertyValue(
|
|
||||||
'--advanced-camera-card-media-layout-view-box-right',
|
|
||||||
),
|
|
||||||
).toBe('6%');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore multiple calls to set same container', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
|
||||||
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
expect(host.getAttribute('size')).toBe('max-width');
|
|
||||||
host.setAttribute('size', 'custom');
|
|
||||||
|
|
||||||
controller.setContainer(container);
|
|
||||||
expect(host.getAttribute('size')).toBe('custom');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reset container', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
|
||||||
|
|
||||||
controller.setContainer(container);
|
|
||||||
controller.setContainer();
|
|
||||||
|
|
||||||
expect(host.getAttribute('size')).toBe('max-width');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should set size attribute correctly', () => {
|
|
||||||
it('should set max-width', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
|
|
||||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
expect(container.style.aspectRatio).toBe('16 / 9');
|
|
||||||
expect(host.getAttribute('size')).toBe('max-width');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set max-height', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
|
|
||||||
controller.setCameraConfig(configWithAspectRatioPortrait);
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
expect(container.style.aspectRatio).toBe('9 / 16');
|
|
||||||
expect(host.getAttribute('size')).toBe('max-height');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set max', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
|
|
||||||
controller.setContainer(container);
|
|
||||||
expect(host.getAttribute('size')).toBe('max');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set max without a config', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
|
|
||||||
controller.setCameraConfig();
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
expect(host.getAttribute('size')).toBe('max');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should respond to size changes', () => {
|
|
||||||
it('should ignore without an aspect ratio', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
host.setAttribute('size', '__RANDOM__');
|
|
||||||
|
|
||||||
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 600,
|
|
||||||
width: 300,
|
|
||||||
});
|
|
||||||
|
|
||||||
new MediaProviderDimensionsController(host);
|
|
||||||
|
|
||||||
callResizeHandler();
|
|
||||||
|
|
||||||
expect(host.getAttribute('size')).toBe('__RANDOM__');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set host to max if no container', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
host.setAttribute('size', 'custom');
|
|
||||||
|
|
||||||
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 600,
|
|
||||||
width: 300,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
|
||||||
|
|
||||||
callResizeHandler();
|
|
||||||
|
|
||||||
expect(host.getAttribute('size')).toBe('max');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set host to max if container has no dimensions', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
host.setAttribute('size', 'custom');
|
|
||||||
|
|
||||||
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 100,
|
|
||||||
width: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 0,
|
|
||||||
width: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
callResizeHandler();
|
|
||||||
|
|
||||||
expect(host.getAttribute('size')).toBe('max');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore resize calls where actual equals intended size', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
host.setAttribute('size', 'custom');
|
|
||||||
|
|
||||||
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 100,
|
|
||||||
width: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 100,
|
|
||||||
width: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
callResizeHandler();
|
|
||||||
|
|
||||||
host.setAttribute('size', '__RANDOM__');
|
|
||||||
|
|
||||||
// 2nd call should be ignored.
|
|
||||||
callResizeHandler();
|
|
||||||
|
|
||||||
expect(host.getAttribute('size')).toBe('__RANDOM__');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should resize container to fit width-limited container', () => {
|
|
||||||
it('should resize container to fit width-limited container', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 200,
|
|
||||||
width: 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 90,
|
|
||||||
width: 160,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
callResizeHandler();
|
|
||||||
|
|
||||||
expect(container.style.width).toBe('100%');
|
|
||||||
expect(container.style.height).toBe('auto');
|
|
||||||
expect(host.getAttribute('size')).toBe('custom');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should resize container to fit height-limited container', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 100,
|
|
||||||
width: 400,
|
|
||||||
});
|
|
||||||
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 200,
|
|
||||||
width: 160,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
callResizeHandler();
|
|
||||||
|
|
||||||
expect(container.style.width).toBe(`${100 * (160 / 200)}px`);
|
|
||||||
expect(container.style.height).toBe('100px');
|
|
||||||
expect(host.getAttribute('size')).toBe('custom');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should respond to media loading', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(
|
|
||||||
requestAnimationFrameMock,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.mocked(window.requestAnimationFrame).mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should resize container after a media load', () => {
|
|
||||||
const host = createLitElement();
|
|
||||||
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 100,
|
|
||||||
width: 400,
|
|
||||||
});
|
|
||||||
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
height: 90,
|
|
||||||
width: 160,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controller = new MediaProviderDimensionsController(host);
|
|
||||||
controller.setCameraConfig(configWithAspectRatioLandscape);
|
|
||||||
controller.setContainer(container);
|
|
||||||
|
|
||||||
controller.hostConnected();
|
|
||||||
|
|
||||||
dispatchExistingMediaLoadedInfoAsEvent(host, createMediaLoadedInfo());
|
|
||||||
|
|
||||||
expect(container.style.width).toBe(`100%`);
|
|
||||||
expect(container.style.height).toBe('auto');
|
|
||||||
expect(host.getAttribute('size')).toBe('custom');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
runWhenIdleIfSupported,
|
runWhenIdleIfSupported,
|
||||||
setify,
|
setify,
|
||||||
setOrRemoveAttribute,
|
setOrRemoveAttribute,
|
||||||
|
setOrRemoveStyleProperty,
|
||||||
} from '../../src/utils/basic.js';
|
} from '../../src/utils/basic.js';
|
||||||
import { createSlot, createSlotHost } from '../test-utils.js';
|
import { createSlot, createSlotHost } from '../test-utils.js';
|
||||||
|
|
||||||
@@ -248,6 +249,27 @@ describe('setOrRemoveAttribute', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setOrRemoveStyleProperty', () => {
|
||||||
|
it('should set style property without value', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
setOrRemoveStyleProperty(element, true, 'width');
|
||||||
|
expect(element.style.getPropertyValue('width')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set style property with value', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
setOrRemoveStyleProperty(element, true, 'width', '100px');
|
||||||
|
expect(element.style.getPropertyValue('width')).toBe('100px');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove style property', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.style.setProperty('width', '100px');
|
||||||
|
setOrRemoveStyleProperty(element, false, 'width');
|
||||||
|
expect(element.style.getPropertyValue('width')).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('isTruthy', () => {
|
describe('isTruthy', () => {
|
||||||
it('should return true for true', () => {
|
it('should return true for true', () => {
|
||||||
expect(isTruthy(true)).toBeTruthy();
|
expect(isTruthy(true)).toBeTruthy();
|
||||||
|
|||||||
Reference in New Issue
Block a user