feat: Allow control of where selected item is in grid (#1979)
- Closes: #1946
This commit is contained in:
@@ -1,10 +1,29 @@
|
|||||||
# Grid Layout Algorithm
|
# Grid Layout Algorithm
|
||||||
|
|
||||||
When display mode (in `live` or `media_viewer` views) is set to `grid`, it will lay out cameras roughly in the order they are specified in the config (items may be moved to optimize grid 'density').
|
When display mode (in [`live`](live.md?id=display) or
|
||||||
|
[`media_viewer`](media-viewer.md?id=display) views) is set to `grid`, this
|
||||||
|
algorithm is used to control the layout.
|
||||||
|
|
||||||
The following algorithm is used to calculate the number of columns. This attempts to offers a balance between configurability, reasonable display in a typical Lovelace card width and reasonable display in a typical fullscreen display.
|
## Layout Order
|
||||||
|
|
||||||
|
Cameras are laid out horizontally in the order they are specified in the config,
|
||||||
|
first to last. The card may tweak item positioning in order to optimize grid
|
||||||
|
'density'.
|
||||||
|
|
||||||
|
In addition, if the `grid_selected_position` parameter is `first` or `last`, the
|
||||||
|
selected camera is always laid out first (at the top) or last (at the bottom) of
|
||||||
|
the layout.
|
||||||
|
|
||||||
|
## Number of columns in the grid
|
||||||
|
|
||||||
|
The following algorithm is used to calculate the number of columns. This
|
||||||
|
attempts to offers a balance between configurability, reasonable display in a
|
||||||
|
typical Lovelace card width and reasonable display in a typical fullscreen
|
||||||
|
display.
|
||||||
|
|
||||||
- Use `grid_columns` if specified.
|
- Use `grid_columns` if specified.
|
||||||
- Otherwise, use the largest number of columns in the range `[2 - grid_max_columns]` that will fit at least a `600px` column width.
|
- Otherwise, use the largest number of columns in the range `[2 -
|
||||||
- Otherwise, use the largest number of columns in the range `[2 - grid_max_columns]` that will fit at least a `190px` column width.
|
grid_max_columns]` that will fit at least a `600px` column width.
|
||||||
|
- Otherwise, use the largest number of columns in the range `[2 -
|
||||||
|
grid_max_columns]` that will fit at least a `190px` column width.
|
||||||
- Otherwise, there will be `1` column only.
|
- Otherwise, there will be `1` column only.
|
||||||
|
|||||||
@@ -153,12 +153,15 @@ live:
|
|||||||
# [...]
|
# [...]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `grid_columns` | | If specified the grid will always have exactly this number of columns. |
|
| `grid_columns` | | If specified the grid will always have exactly this number of columns. |
|
||||||
| `grid_max_columns` | `4` | If specified, and `grid_columns` is not specified, the grid will not render more than this number of columns. The precise number will be calculated based on the [grid layout algorithm](grid-layout-algorithm.md). |
|
| `grid_max_columns` | `4` | If specified, and `grid_columns` is not specified, the grid will not render more than this number of columns. |
|
||||||
| `grid_selected_width_factor` | `2` | How much to scale up the selected media item in a grid. A value of `1` will not scale the selected item at all, the default value of `2` will scale the media item width to twice what it would otherwise be, etc. |
|
| `grid_selected_position` | `default` | Controls where the selected item should be laid out in the grid. If `default`, the cameras are laid out in the order they are specified in the configuration and selecting a camera does not change this order. If `first`, the selected camera is moved to the start of the grid, if `last` it is moved to the end of the grid. |
|
||||||
| `mode` | `single` | Whether to display a `single` live camera in a carousel, or all cameras in a `grid` configuration. |
|
| `grid_selected_width_factor` | `2` | How much to scale up the selected media item in a grid. A value of `1` will not scale the selected item at all, the default value of `2` will scale the media item width to twice what it would otherwise be, etc. |
|
||||||
|
| `mode` | `single` | Whether to display a `single` live camera in a carousel, or all cameras in a `grid` configuration. |
|
||||||
|
|
||||||
|
See the [grid layout algorithm](grid-layout-algorithm.md) for more details on how the grid lays elements out.
|
||||||
|
|
||||||
## `microphone`
|
## `microphone`
|
||||||
|
|
||||||
@@ -238,6 +241,7 @@ live:
|
|||||||
mute_after_microphone_mute_seconds: 60
|
mute_after_microphone_mute_seconds: 60
|
||||||
display:
|
display:
|
||||||
mode: single
|
mode: single
|
||||||
|
grid_selected_position: default
|
||||||
grid_selected_width_factor: 2
|
grid_selected_width_factor: 2
|
||||||
grid_max_columns: 4
|
grid_max_columns: 4
|
||||||
actions:
|
actions:
|
||||||
|
|||||||
@@ -147,12 +147,15 @@ media_viewer:
|
|||||||
# [...]
|
# [...]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `grid_columns` | | If specified the grid will always have exactly this number of columns. |
|
| `grid_columns` | | If specified the grid will always have exactly this number of columns. |
|
||||||
| `grid_max_columns` | `4` | If specified, and `grid_columns` is not specified, the grid will not render more than this number of columns. The precise number will be calculated based on the [grid layout algorithm](grid-layout-algorithm.md). |
|
| `grid_max_columns` | `4` | If specified, and `grid_columns` is not specified, the grid will not render more than this number of columns. |
|
||||||
| `grid_selected_width_factor` | `2` | How much to scale up the selected media item in a grid. A value of `1` will not scale the selected item at all, the default value of `2` will scale the media item width to twice what it would otherwise be, etc. |
|
| `grid_selected_position` | `default` | Controls where the selected item should be laid out in the grid. If `default`, the cameras are laid out in the order they are specified in the configuration and selecting a camera does not change this order. If `first`, the selected camera is moved to the start of the grid, if `last` it is moved to the end of the grid. |
|
||||||
| `mode` | `single` | Whether to display a `single` media item at a time, or a media item for all cameras in a `grid` configuration. |
|
| `grid_selected_width_factor` | `2` | How much to scale up the selected media item in a grid. A value of `1` will not scale the selected item at all, the default value of `2` will scale the media item width to twice what it would otherwise be, etc. |
|
||||||
|
| `mode` | `single` | Whether to display a `single` media item at a time, or a media item for all cameras in a `grid` configuration. |
|
||||||
|
|
||||||
|
See the [grid layout algorithm](grid-layout-algorithm.md) for more details on how the grid lays elements out.
|
||||||
|
|
||||||
## Fully expanded reference
|
## Fully expanded reference
|
||||||
|
|
||||||
@@ -209,6 +212,7 @@ media_viewer:
|
|||||||
24h: true
|
24h: true
|
||||||
display:
|
display:
|
||||||
mode: single
|
mode: single
|
||||||
|
grid_selected_position: default
|
||||||
grid_selected_width_factor: 2
|
grid_selected_width_factor: 2
|
||||||
grid_max_columns: 4
|
grid_max_columns: 4
|
||||||
actions:
|
actions:
|
||||||
|
|||||||
@@ -34,13 +34,21 @@ export interface MediaGridConstructorOptions {
|
|||||||
displayConfig?: ViewDisplayConfig;
|
displayConfig?: ViewDisplayConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExtendedMasonry extends Masonry {
|
||||||
|
// Expose the items array to allow for custom ordering (used for the
|
||||||
|
// `grid_selected_position` parameter).
|
||||||
|
items: {
|
||||||
|
element: MediaGridChild;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
export class MediaGridController {
|
export class MediaGridController {
|
||||||
protected _host: HTMLElement;
|
protected _host: HTMLElement;
|
||||||
|
|
||||||
protected _selected: GridID | null;
|
protected _selected: GridID | null;
|
||||||
protected _mediaLoadedInfoMap: Map<GridID, MediaLoadedInfo> = new Map();
|
protected _mediaLoadedInfoMap: Map<GridID, MediaLoadedInfo> = new Map();
|
||||||
protected _gridContents: MediaGridContents = new Map();
|
protected _gridContents: MediaGridContents = new Map();
|
||||||
protected _masonry: Masonry | null = null;
|
protected _masonry: ExtendedMasonry | null = null;
|
||||||
protected _displayConfig: ViewDisplayConfig | null = null;
|
protected _displayConfig: ViewDisplayConfig | null = null;
|
||||||
protected _hostWidth: number;
|
protected _hostWidth: number;
|
||||||
protected _idAttribute: string;
|
protected _idAttribute: string;
|
||||||
@@ -128,6 +136,34 @@ export class MediaGridController {
|
|||||||
return this._selected;
|
return this._selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _sortItemsInGrid(): void {
|
||||||
|
const existingItems = this._masonry?.items;
|
||||||
|
const selectedItem = existingItems?.find(
|
||||||
|
(item) => item.element.getAttribute(this._idAttribute) === this._selected,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If `grid_selected_position` is set to 'first' or 'last', move the
|
||||||
|
// selected item to the start or end of the list respectively.
|
||||||
|
if (
|
||||||
|
!!this._displayConfig?.grid_selected_position &&
|
||||||
|
['first', 'last'].includes(this._displayConfig.grid_selected_position) &&
|
||||||
|
existingItems &&
|
||||||
|
selectedItem &&
|
||||||
|
this._masonry
|
||||||
|
) {
|
||||||
|
// Implementation note: With the latest version of the Masonry library
|
||||||
|
// (4.2.2) using the prepended() and appended() methods in quick sucession
|
||||||
|
// causes the layout to not show the newly added items. Instead, access
|
||||||
|
// the items in place and swap them around.
|
||||||
|
const otherItems = existingItems?.filter((item) => item !== selectedItem);
|
||||||
|
const newItems =
|
||||||
|
this._displayConfig.grid_selected_position === 'first'
|
||||||
|
? [selectedItem, ...otherItems]
|
||||||
|
: [...otherItems, selectedItem];
|
||||||
|
this._masonry.items = newItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public selectCell(id: GridID) {
|
public selectCell(id: GridID) {
|
||||||
if (this._selected === id) {
|
if (this._selected === id) {
|
||||||
return;
|
return;
|
||||||
@@ -141,11 +177,12 @@ export class MediaGridController {
|
|||||||
dispatchExistingMediaLoadedInfoAsEvent(this._host, mediaLoadedInfo);
|
dispatchExistingMediaLoadedInfoAsEvent(this._host, mediaLoadedInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._sortItemsInGrid();
|
||||||
this._updateSelectedStylesOnElements();
|
this._updateSelectedStylesOnElements();
|
||||||
|
|
||||||
// Sizes may change when an element is selected, so re-do the layout (must
|
// Sizes and positions may change when an element is selected, so re-do the
|
||||||
// come after the call to _updateStylesOnElements in order to ensure the
|
// layout (must come after the call to _updateStylesOnElements in order to
|
||||||
// right styles are applied first).
|
// ensure the right styles are applied first).
|
||||||
this._throttledLayout();
|
this._throttledLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +240,7 @@ export class MediaGridController {
|
|||||||
this._cellResizeObserver.observe(child);
|
this._cellResizeObserver.observe(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._sortItemsInGrid();
|
||||||
this._updateSelectedStylesOnElements();
|
this._updateSelectedStylesOnElements();
|
||||||
this._setColumnSizeStyles();
|
this._setColumnSizeStyles();
|
||||||
}
|
}
|
||||||
@@ -275,7 +313,7 @@ export class MediaGridController {
|
|||||||
percentPosition: true,
|
percentPosition: true,
|
||||||
transitionDuration: '0.2s',
|
transitionDuration: '0.2s',
|
||||||
gutter: MEDIA_GRID_HORIZONTAL_GUTTER_WIDTH,
|
gutter: MEDIA_GRID_HORIZONTAL_GUTTER_WIDTH,
|
||||||
});
|
}) as ExtendedMasonry;
|
||||||
this._masonry.addItems?.([...this._gridContents.values()]);
|
this._masonry.addItems?.([...this._gridContents.values()]);
|
||||||
this._throttledLayout();
|
this._throttledLayout();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import { z } from 'zod';
|
|||||||
export const viewDisplayModeSchema = z.enum(['single', 'grid']);
|
export const viewDisplayModeSchema = z.enum(['single', 'grid']);
|
||||||
export type ViewDisplayMode = z.infer<typeof viewDisplayModeSchema>;
|
export type ViewDisplayMode = z.infer<typeof viewDisplayModeSchema>;
|
||||||
|
|
||||||
|
const gridSelectedPositionSchema = z.enum(['default', 'first', 'last']);
|
||||||
|
|
||||||
export const viewDisplaySchema = z
|
export const viewDisplaySchema = z
|
||||||
.object({
|
.object({
|
||||||
mode: viewDisplayModeSchema.optional(),
|
mode: viewDisplayModeSchema.optional(),
|
||||||
|
grid_selected_position: gridSelectedPositionSchema.optional(),
|
||||||
grid_selected_width_factor: z.number().min(0).optional(),
|
grid_selected_width_factor: z.number().min(0).optional(),
|
||||||
grid_max_columns: z.number().min(0).optional(),
|
grid_max_columns: z.number().min(0).optional(),
|
||||||
grid_columns: z.number().min(0).optional(),
|
grid_columns: z.number().min(0).optional(),
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ export const CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS =
|
|||||||
`${CONF_MEDIA_VIEWER}.display.grid_columns` as const;
|
`${CONF_MEDIA_VIEWER}.display.grid_columns` as const;
|
||||||
export const CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS =
|
export const CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS =
|
||||||
`${CONF_MEDIA_VIEWER}.display.grid_max_columns` as const;
|
`${CONF_MEDIA_VIEWER}.display.grid_max_columns` as const;
|
||||||
|
export const CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_POSITION =
|
||||||
|
`${CONF_MEDIA_VIEWER}.display.grid_selected_position` as const;
|
||||||
export const CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_WIDTH_FACTOR =
|
export const CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_WIDTH_FACTOR =
|
||||||
`${CONF_MEDIA_VIEWER}.display.grid_selected_width_factor` as const;
|
`${CONF_MEDIA_VIEWER}.display.grid_selected_width_factor` as const;
|
||||||
export const CONF_MEDIA_VIEWER_DRAGGABLE = `${CONF_MEDIA_VIEWER}.draggable` as const;
|
export const CONF_MEDIA_VIEWER_DRAGGABLE = `${CONF_MEDIA_VIEWER}.draggable` as const;
|
||||||
@@ -294,6 +296,8 @@ export const CONF_LIVE_DISPLAY_GRID_COLUMNS =
|
|||||||
`${CONF_LIVE}.display.grid_columns` as const;
|
`${CONF_LIVE}.display.grid_columns` as const;
|
||||||
export const CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS =
|
export const CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS =
|
||||||
`${CONF_LIVE}.display.grid_max_columns` as const;
|
`${CONF_LIVE}.display.grid_max_columns` as const;
|
||||||
|
export const CONF_LIVE_DISPLAY_GRID_SELECTED_POSITION =
|
||||||
|
`${CONF_LIVE}.display.grid_selected_position` as const;
|
||||||
export const CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR =
|
export const CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR =
|
||||||
`${CONF_LIVE}.display.grid_selected_width_factor` as const;
|
`${CONF_LIVE}.display.grid_selected_width_factor` as const;
|
||||||
export const CONF_LIVE_DRAGGABLE = `${CONF_LIVE}.draggable` as const;
|
export const CONF_LIVE_DRAGGABLE = `${CONF_LIVE}.draggable` as const;
|
||||||
|
|||||||
+28
-4
@@ -126,6 +126,7 @@ import {
|
|||||||
CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS,
|
CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS,
|
||||||
CONF_LIVE_DISPLAY_GRID_COLUMNS,
|
CONF_LIVE_DISPLAY_GRID_COLUMNS,
|
||||||
CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS,
|
CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS,
|
||||||
|
CONF_LIVE_DISPLAY_GRID_SELECTED_POSITION,
|
||||||
CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
|
CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
|
||||||
CONF_LIVE_DISPLAY_MODE,
|
CONF_LIVE_DISPLAY_MODE,
|
||||||
CONF_LIVE_DRAGGABLE,
|
CONF_LIVE_DRAGGABLE,
|
||||||
@@ -167,6 +168,7 @@ import {
|
|||||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS,
|
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS,
|
||||||
CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS,
|
CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS,
|
||||||
CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS,
|
CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS,
|
||||||
|
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_POSITION,
|
||||||
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
|
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
|
||||||
CONF_MEDIA_VIEWER_DISPLAY_MODE,
|
CONF_MEDIA_VIEWER_DISPLAY_MODE,
|
||||||
CONF_MEDIA_VIEWER_DRAGGABLE,
|
CONF_MEDIA_VIEWER_DRAGGABLE,
|
||||||
@@ -649,6 +651,22 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
{ value: 'grid', label: localize('display_modes.grid') },
|
{ value: 'grid', label: localize('display_modes.grid') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected _gridSelectPositions: EditorSelectOption[] = [
|
||||||
|
{ value: '', label: '' },
|
||||||
|
{
|
||||||
|
value: 'default',
|
||||||
|
label: localize('config.common.display.grid_selected_positions.default'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'first',
|
||||||
|
label: localize('config.common.display.grid_selected_positions.first'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'last',
|
||||||
|
label: localize('config.common.display.grid_selected_positions.last'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
protected _castMethods: EditorSelectOption[] = [
|
protected _castMethods: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'standard', label: localize('config.cameras.cast.methods.standard') },
|
{ value: 'standard', label: localize('config.cameras.cast.methods.standard') },
|
||||||
@@ -1651,14 +1669,11 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
protected _renderViewDisplay(
|
protected _renderViewDisplay(
|
||||||
domain: string,
|
domain: string,
|
||||||
configPathMode: string,
|
configPathMode: string,
|
||||||
|
configPathSelectedPosition: string,
|
||||||
configPathSelectedWidthFactor: string,
|
configPathSelectedWidthFactor: string,
|
||||||
configPathColumns: string,
|
configPathColumns: string,
|
||||||
configPathMaxColumns: string,
|
configPathMaxColumns: string,
|
||||||
): TemplateResult | void {
|
): TemplateResult | void {
|
||||||
// grid_select_width_factor: z.number().min(0).optional(),
|
|
||||||
// grid_max_columns: z.number().min(0).optional(),
|
|
||||||
// grid_columns: z.number().min(0).optional(),
|
|
||||||
|
|
||||||
return this._putInSubmenu(
|
return this._putInSubmenu(
|
||||||
domain,
|
domain,
|
||||||
true,
|
true,
|
||||||
@@ -1668,6 +1683,13 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
${this._renderOptionSelector(configPathMode, this._displayModes, {
|
${this._renderOptionSelector(configPathMode, this._displayModes, {
|
||||||
label: localize('config.common.display.mode'),
|
label: localize('config.common.display.mode'),
|
||||||
})}
|
})}
|
||||||
|
${this._renderOptionSelector(
|
||||||
|
configPathSelectedPosition,
|
||||||
|
this._gridSelectPositions,
|
||||||
|
{
|
||||||
|
label: localize('config.common.display.grid_selected_position'),
|
||||||
|
},
|
||||||
|
)}
|
||||||
${this._renderNumberInput(configPathSelectedWidthFactor, {
|
${this._renderNumberInput(configPathSelectedWidthFactor, {
|
||||||
min: 0,
|
min: 0,
|
||||||
label: localize('config.common.display.grid_selected_width_factor'),
|
label: localize('config.common.display.grid_selected_width_factor'),
|
||||||
@@ -2711,6 +2733,7 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
${this._renderViewDisplay(
|
${this._renderViewDisplay(
|
||||||
MENU_LIVE_DISPLAY,
|
MENU_LIVE_DISPLAY,
|
||||||
CONF_LIVE_DISPLAY_MODE,
|
CONF_LIVE_DISPLAY_MODE,
|
||||||
|
CONF_LIVE_DISPLAY_GRID_SELECTED_POSITION,
|
||||||
CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
|
CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
|
||||||
CONF_LIVE_DISPLAY_GRID_COLUMNS,
|
CONF_LIVE_DISPLAY_GRID_COLUMNS,
|
||||||
CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS,
|
CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS,
|
||||||
@@ -2898,6 +2921,7 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
${this._renderViewDisplay(
|
${this._renderViewDisplay(
|
||||||
MENU_MEDIA_VIEWER_DISPLAY,
|
MENU_MEDIA_VIEWER_DISPLAY,
|
||||||
CONF_MEDIA_VIEWER_DISPLAY_MODE,
|
CONF_MEDIA_VIEWER_DISPLAY_MODE,
|
||||||
|
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_POSITION,
|
||||||
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
|
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
|
||||||
CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS,
|
CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS,
|
||||||
CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS,
|
CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS,
|
||||||
|
|||||||
@@ -259,6 +259,12 @@
|
|||||||
"editor_label": "Visualització",
|
"editor_label": "Visualització",
|
||||||
"grid_columns": "Nombre exacte de columnes de la graella",
|
"grid_columns": "Nombre exacte de columnes de la graella",
|
||||||
"grid_max_columns": "Nombre màxim de columnes de la graella",
|
"grid_max_columns": "Nombre màxim de columnes de la graella",
|
||||||
|
"grid_selected_position": "",
|
||||||
|
"grid_selected_positions": {
|
||||||
|
"default": "",
|
||||||
|
"first": "",
|
||||||
|
"last": ""
|
||||||
|
},
|
||||||
"grid_selected_width_factor": "Augmenta l'amplada del suport multimèdia seleccionat en aquest factor",
|
"grid_selected_width_factor": "Augmenta l'amplada del suport multimèdia seleccionat en aquest factor",
|
||||||
"mode": "Mode"
|
"mode": "Mode"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -237,8 +237,8 @@
|
|||||||
"timeline": {
|
"timeline": {
|
||||||
"editor_label": "Mini Timeline",
|
"editor_label": "Mini Timeline",
|
||||||
"format": {
|
"format": {
|
||||||
"editor_label": "Time & date format",
|
"24h": "Use 24-hour clock",
|
||||||
"24h": "Use 24-hour clock"
|
"editor_label": "Time & date format"
|
||||||
},
|
},
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
"modes": {
|
"modes": {
|
||||||
@@ -259,6 +259,12 @@
|
|||||||
"editor_label": "Display",
|
"editor_label": "Display",
|
||||||
"grid_columns": "Exact number of grid columns",
|
"grid_columns": "Exact number of grid columns",
|
||||||
"grid_max_columns": "Maximum number of grid columns",
|
"grid_max_columns": "Maximum number of grid columns",
|
||||||
|
"grid_selected_position": "Position of selected media in grid",
|
||||||
|
"grid_selected_positions": {
|
||||||
|
"default": "Selected item in default position",
|
||||||
|
"first": "Selected item is first in grid",
|
||||||
|
"last": "Selected item is last in grid"
|
||||||
|
},
|
||||||
"grid_selected_width_factor": "Increase selected media width by this factor",
|
"grid_selected_width_factor": "Increase selected media width by this factor",
|
||||||
"mode": "Mode"
|
"mode": "Mode"
|
||||||
},
|
},
|
||||||
@@ -465,8 +471,8 @@
|
|||||||
},
|
},
|
||||||
"remote_control": {
|
"remote_control": {
|
||||||
"entities": {
|
"entities": {
|
||||||
"editor_label": "Remote Control Entities",
|
"camera": "Input Select entity to control camera",
|
||||||
"camera": "Input Select entity to control camera"
|
"editor_label": "Remote Control Entities"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status_bar": {
|
"status_bar": {
|
||||||
@@ -528,8 +534,8 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"themes": {
|
"themes": {
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"ha": "Home Assistant",
|
|
||||||
"editor_label": "Themes",
|
"editor_label": "Themes",
|
||||||
|
"ha": "Home Assistant",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"traditional": "Traditional"
|
"traditional": "Traditional"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,6 +259,12 @@
|
|||||||
"editor_label": "Affichage",
|
"editor_label": "Affichage",
|
||||||
"grid_columns": "Nombre de colonnes de la grille",
|
"grid_columns": "Nombre de colonnes de la grille",
|
||||||
"grid_max_columns": "Nombre maximum de colonnes de la grille",
|
"grid_max_columns": "Nombre maximum de colonnes de la grille",
|
||||||
|
"grid_selected_position": "",
|
||||||
|
"grid_selected_positions": {
|
||||||
|
"default": "",
|
||||||
|
"first": "",
|
||||||
|
"last": ""
|
||||||
|
},
|
||||||
"grid_selected_width_factor": "Augmenter la largeur du média sélectionnée par ce facteur",
|
"grid_selected_width_factor": "Augmenter la largeur du média sélectionnée par ce facteur",
|
||||||
"mode": "Mode"
|
"mode": "Mode"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -259,6 +259,12 @@
|
|||||||
"editor_label": "",
|
"editor_label": "",
|
||||||
"grid_columns": "",
|
"grid_columns": "",
|
||||||
"grid_max_columns": "",
|
"grid_max_columns": "",
|
||||||
|
"grid_selected_position": "",
|
||||||
|
"grid_selected_positions": {
|
||||||
|
"default": "",
|
||||||
|
"first": "",
|
||||||
|
"last": ""
|
||||||
|
},
|
||||||
"grid_selected_width_factor": "",
|
"grid_selected_width_factor": "",
|
||||||
"mode": ""
|
"mode": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -259,6 +259,12 @@
|
|||||||
"editor_label": "",
|
"editor_label": "",
|
||||||
"grid_columns": "",
|
"grid_columns": "",
|
||||||
"grid_max_columns": "",
|
"grid_max_columns": "",
|
||||||
|
"grid_selected_position": "",
|
||||||
|
"grid_selected_positions": {
|
||||||
|
"default": "",
|
||||||
|
"first": "",
|
||||||
|
"last": ""
|
||||||
|
},
|
||||||
"grid_selected_width_factor": "",
|
"grid_selected_width_factor": "",
|
||||||
"mode": ""
|
"mode": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -259,6 +259,12 @@
|
|||||||
"editor_label": "",
|
"editor_label": "",
|
||||||
"grid_columns": "",
|
"grid_columns": "",
|
||||||
"grid_max_columns": "",
|
"grid_max_columns": "",
|
||||||
|
"grid_selected_position": "",
|
||||||
|
"grid_selected_positions": {
|
||||||
|
"default": "",
|
||||||
|
"first": "",
|
||||||
|
"last": ""
|
||||||
|
},
|
||||||
"grid_selected_width_factor": "",
|
"grid_selected_width_factor": "",
|
||||||
"mode": ""
|
"mode": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import Masonry from 'masonry-layout';
|
import Masonry from 'masonry-layout';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { MediaLoadedInfo } from '../../src/types';
|
|
||||||
import {
|
import {
|
||||||
|
ExtendedMasonry,
|
||||||
MediaGridConstructorOptions,
|
MediaGridConstructorOptions,
|
||||||
MediaGridController,
|
MediaGridController,
|
||||||
} from '../../src/components-lib/media-grid-controller';
|
} from '../../src/components-lib/media-grid-controller';
|
||||||
|
import { MediaLoadedInfo } from '../../src/types';
|
||||||
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../src/utils/media-info';
|
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../src/utils/media-info';
|
||||||
import {
|
import {
|
||||||
MutationObserverMock,
|
MutationObserverMock,
|
||||||
@@ -18,7 +19,7 @@ vi.mock('lodash-es/throttle', () => ({
|
|||||||
default: vi.fn((fn) => fn),
|
default: vi.fn((fn) => fn),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const masonry = mock<Masonry>();
|
const masonry = mock<ExtendedMasonry>();
|
||||||
vi.mock('masonry-layout', () => ({
|
vi.mock('masonry-layout', () => ({
|
||||||
default: vi.fn().mockImplementation(() => {
|
default: vi.fn().mockImplementation(() => {
|
||||||
return masonry;
|
return masonry;
|
||||||
@@ -85,6 +86,8 @@ describe('MediaGridController', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
vi.stubGlobal('MutationObserver', MutationObserverMock);
|
vi.stubGlobal('MutationObserver', MutationObserverMock);
|
||||||
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
||||||
|
|
||||||
|
masonry.items = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be constructable', () => {
|
it('should be constructable', () => {
|
||||||
@@ -526,4 +529,63 @@ describe('MediaGridController', () => {
|
|||||||
expect(Masonry).not.toBeCalled();
|
expect(Masonry).not.toBeCalled();
|
||||||
expect(masonry.layout).not.toBeCalled();
|
expect(masonry.layout).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('describe should sort grid elements correctly', () => {
|
||||||
|
it('should respect placement when grid_selected_position is default', () => {
|
||||||
|
const children = createChildren();
|
||||||
|
const parent = createParent({ children: children });
|
||||||
|
|
||||||
|
// Simulate wrapped children in masonry object.
|
||||||
|
masonry.items = children.map((child) => ({ element: child }));
|
||||||
|
|
||||||
|
const controller = createController(parent);
|
||||||
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_position: 'default' });
|
||||||
|
|
||||||
|
controller.selectCell('1');
|
||||||
|
|
||||||
|
expect(masonry.items).toEqual([
|
||||||
|
{ element: children[0] },
|
||||||
|
{ element: children[1] },
|
||||||
|
{ element: children[2] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect placement when grid_selected_position is first', () => {
|
||||||
|
const children = createChildren(['0', '1', '2']);
|
||||||
|
const parent = createParent({ children: children });
|
||||||
|
|
||||||
|
// Simulate wrapped children in masonry object.
|
||||||
|
masonry.items = children.map((child) => ({ element: child }));
|
||||||
|
|
||||||
|
const controller = createController(parent);
|
||||||
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_position: 'first' });
|
||||||
|
|
||||||
|
controller.selectCell('1');
|
||||||
|
|
||||||
|
expect(masonry.items).toEqual([
|
||||||
|
{ element: children[1] },
|
||||||
|
{ element: children[0] },
|
||||||
|
{ element: children[2] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect placement when grid_selected_position is last', () => {
|
||||||
|
const children = createChildren(['0', '1', '2']);
|
||||||
|
const parent = createParent({ children: children });
|
||||||
|
|
||||||
|
// Simulate wrapped children in masonry object.
|
||||||
|
masonry.items = children.map((child) => ({ element: child }));
|
||||||
|
|
||||||
|
const controller = createController(parent);
|
||||||
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_position: 'last' });
|
||||||
|
|
||||||
|
controller.selectCell('1');
|
||||||
|
|
||||||
|
expect(masonry.items).toEqual([
|
||||||
|
{ element: children[0] },
|
||||||
|
{ element: children[2] },
|
||||||
|
{ element: children[1] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user