Allow a card to span the full width of a section (#21758)

* Limit card size with the grid size

* Set full option in YAML

* Export card grid size

* Add editor

* Set full column for map card and iframe by default

* Do not set string variable
This commit is contained in:
Paul Bottein 2024-08-28 12:01:40 +02:00 committed by GitHub
parent 5a229e3c88
commit 2f68ee0efc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 157 additions and 77 deletions

View File

@ -1,24 +1,24 @@
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "./ha-icon-button";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider"; import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import "./ha-icon-button";
import { mdiRestore } from "@mdi/js"; import { mdiRestore } from "@mdi/js";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import { conditionalClamp } from "../common/number/clamp"; import { conditionalClamp } from "../common/number/clamp";
import {
type GridSizeValue = { CardGridSize,
rows?: number | "auto"; DEFAULT_GRID_SIZE,
columns?: number; } from "../panels/lovelace/common/compute-card-grid-size";
}; import { HomeAssistant } from "../types";
@customElement("ha-grid-size-picker") @customElement("ha-grid-size-picker")
export class HaGridSizeEditor extends LitElement { export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: GridSizeValue; @property({ attribute: false }) public value?: CardGridSize;
@property({ attribute: false }) public rows = 8; @property({ attribute: false }) public rows = 8;
@ -34,7 +34,7 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public isDefault?: boolean; @property({ attribute: false }) public isDefault?: boolean;
@state() public _localValue?: GridSizeValue = undefined; @state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
protected willUpdate(changedProperties) { protected willUpdate(changedProperties) {
if (changedProperties.has("value")) { if (changedProperties.has("value")) {
@ -49,6 +49,7 @@ export class HaGridSizeEditor extends LitElement {
this.rowMin !== undefined && this.rowMin === this.rowMax; this.rowMin !== undefined && this.rowMin === this.rowMax;
const autoHeight = this._localValue?.rows === "auto"; const autoHeight = this._localValue?.rows === "auto";
const fullWidth = this._localValue?.columns === "full";
const rowMin = this.rowMin ?? 1; const rowMin = this.rowMin ?? 1;
const rowMax = this.rowMax ?? this.rows; const rowMax = this.rowMax ?? this.rows;
@ -67,7 +68,7 @@ export class HaGridSizeEditor extends LitElement {
.min=${columnMin} .min=${columnMin}
.max=${columnMax} .max=${columnMax}
.range=${this.columns} .range=${this.columns}
.value=${columnValue} .value=${fullWidth ? this.columns : columnValue}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved} @slider-moved=${this._sliderMoved}
.disabled=${disabledColumns} .disabled=${disabledColumns}
@ -104,12 +105,12 @@ export class HaGridSizeEditor extends LitElement {
` `
: nothing} : nothing}
<div <div
class="preview" class="preview ${classMap({ "full-width": fullWidth })}"
style=${styleMap({ style=${styleMap({
"--total-rows": this.rows, "--total-rows": this.rows,
"--total-columns": this.columns, "--total-columns": this.columns,
"--rows": rowValue, "--rows": rowValue,
"--columns": columnValue, "--columns": fullWidth ? this.columns : columnValue,
})} })}
> >
<div> <div>
@ -140,12 +141,21 @@ export class HaGridSizeEditor extends LitElement {
const cell = ev.currentTarget as HTMLElement; const cell = ev.currentTarget as HTMLElement;
const rows = Number(cell.getAttribute("data-row")); const rows = Number(cell.getAttribute("data-row"));
const columns = Number(cell.getAttribute("data-column")); const columns = Number(cell.getAttribute("data-column"));
const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax); const clampedRow: CardGridSize["rows"] = conditionalClamp(
const clampedColumn = conditionalClamp( rows,
this.rowMin,
this.rowMax
);
let clampedColumn: CardGridSize["columns"] = conditionalClamp(
columns, columns,
this.columnMin, this.columnMin,
this.columnMax this.columnMax
); );
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
if (currentSize.columns === "full" && clampedColumn === this.columns) {
clampedColumn = "full";
}
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { rows: clampedRow, columns: clampedColumn }, value: { rows: clampedRow, columns: clampedColumn },
}); });
@ -153,12 +163,23 @@ export class HaGridSizeEditor extends LitElement {
private _valueChanged(ev) { private _valueChanged(ev) {
ev.stopPropagation(); ev.stopPropagation();
const key = ev.currentTarget.id; const key = ev.currentTarget.id as "rows" | "columns";
const newValue = { const currentSize = this.value ?? DEFAULT_GRID_SIZE;
...this.value, let value = ev.detail.value as CardGridSize[typeof key];
[key]: ev.detail.value,
if (
key === "columns" &&
currentSize.columns === "full" &&
value === this.columns
) {
value = "full";
}
const newSize = {
...currentSize,
[key]: value,
}; };
fireEvent(this, "value-changed", { value: newValue }); fireEvent(this, "value-changed", { value: newSize });
} }
private _reset(ev) { private _reset(ev) {
@ -173,11 +194,14 @@ export class HaGridSizeEditor extends LitElement {
private _sliderMoved(ev) { private _sliderMoved(ev) {
ev.stopPropagation(); ev.stopPropagation();
const key = ev.currentTarget.id; const key = ev.currentTarget.id as "rows" | "columns";
const value = ev.detail.value; const currentSize = this.value ?? DEFAULT_GRID_SIZE;
const value = ev.detail.value as CardGridSize[typeof key] | undefined;
if (value === undefined) return; if (value === undefined) return;
this._localValue = { this._localValue = {
...this.value, ...currentSize,
[key]: ev.detail.value, [key]: ev.detail.value,
}; };
} }
@ -231,10 +255,13 @@ export class HaGridSizeEditor extends LitElement {
} }
.selected .cell { .selected .cell {
background-color: var(--primary-color); background-color: var(--primary-color);
grid-column: 1 / span var(--columns, 0); grid-column: 1 / span min(var(--columns, 0), var(--total-columns));
grid-row: 1 / span var(--rows, 0); grid-row: 1 / span min(var(--rows, 0), var(--total-rows));
opacity: 0.5; opacity: 0.5;
} }
.preview.full-width .selected .cell {
grid-column: 1 / -1;
}
`, `,
]; ];
} }

View File

@ -114,7 +114,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
public getLayoutOptions(): LovelaceLayoutOptions { public getLayoutOptions(): LovelaceLayoutOptions {
return { return {
grid_columns: 4, grid_columns: "full",
grid_rows: 4, grid_rows: 4,
grid_min_rows: 2, grid_min_rows: 2,
}; };

View File

@ -426,7 +426,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
public getLayoutOptions(): LovelaceLayoutOptions { public getLayoutOptions(): LovelaceLayoutOptions {
return { return {
grid_columns: 4, grid_columns: "full",
grid_rows: 4, grid_rows: 4,
grid_min_columns: 2, grid_min_columns: 2,
grid_min_rows: 2, grid_min_rows: 2,

View File

@ -0,0 +1,36 @@
import { conditionalClamp } from "../../../common/number/clamp";
import { LovelaceLayoutOptions } from "../types";
export const DEFAULT_GRID_SIZE = {
columns: 4,
rows: "auto",
} as CardGridSize;
export type CardGridSize = {
rows: number | "auto";
columns: number | "full";
};
export const computeCardGridSize = (
options: LovelaceLayoutOptions
): CardGridSize => {
const rows = options.grid_rows ?? DEFAULT_GRID_SIZE.rows;
const columns = options.grid_columns ?? DEFAULT_GRID_SIZE.columns;
const minRows = options.grid_min_rows;
const maxRows = options.grid_max_rows;
const minColumns = options.grid_min_columns;
const maxColumns = options.grid_max_columns;
const clampedRows =
typeof rows === "string" ? rows : conditionalClamp(rows, minRows, maxRows);
const clampedColumns =
typeof columns === "string"
? columns
: conditionalClamp(columns, minColumns, maxColumns);
return {
rows: clampedRows,
columns: clampedColumns,
};
};

View File

@ -1,6 +1,6 @@
import type { ActionDetail } from "@material/mwc-list"; import type { ActionDetail } from "@material/mwc-list";
import { mdiCheck, mdiDotsVertical } from "@mdi/js"; import { mdiCheck, mdiDotsVertical } from "@mdi/js";
import { LitElement, PropertyValues, css, html, nothing } from "lit"; import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
@ -8,6 +8,7 @@ import { preventDefault } from "../../../../common/dom/prevent_default";
import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-formfield";
import "../../../../components/ha-grid-size-picker"; import "../../../../components/ha-grid-size-picker";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item"; import "../../../../components/ha-list-item";
@ -21,7 +22,10 @@ import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { HuiCard } from "../../cards/hui-card"; import { HuiCard } from "../../cards/hui-card";
import { computeSizeOnGrid } from "../../sections/hui-grid-section"; import {
CardGridSize,
computeCardGridSize,
} from "../../common/compute-card-grid-size";
import { LovelaceLayoutOptions } from "../../types"; import { LovelaceLayoutOptions } from "../../types";
@customElement("hui-card-layout-editor") @customElement("hui-card-layout-editor")
@ -50,7 +54,7 @@ export class HuiCardLayoutEditor extends LitElement {
}) })
); );
private _gridSizeValue = memoizeOne(computeSizeOnGrid); private _computeCardGridSize = memoizeOne(computeCardGridSize);
private _isDefault = memoizeOne( private _isDefault = memoizeOne(
(options?: LovelaceLayoutOptions) => (options?: LovelaceLayoutOptions) =>
@ -63,7 +67,7 @@ export class HuiCardLayoutEditor extends LitElement {
this._defaultLayoutOptions this._defaultLayoutOptions
); );
const sizeValue = this._gridSizeValue(options); const value = this._computeCardGridSize(options);
return html` return html`
<div class="header"> <div class="header">
@ -128,7 +132,7 @@ export class HuiCardLayoutEditor extends LitElement {
: html` : html`
<ha-grid-size-picker <ha-grid-size-picker
.hass=${this.hass} .hass=${this.hass}
.value=${sizeValue} .value=${value}
.isDefault=${this._isDefault(this.config.layout_options)} .isDefault=${this._isDefault(this.config.layout_options)}
@value-changed=${this._gridSizeChanged} @value-changed=${this._gridSizeChanged}
.rowMin=${options.grid_min_rows} .rowMin=${options.grid_min_rows}
@ -136,6 +140,24 @@ export class HuiCardLayoutEditor extends LitElement {
.columnMin=${options.grid_min_columns} .columnMin=${options.grid_min_columns}
.columnMax=${options.grid_max_columns} .columnMax=${options.grid_max_columns}
></ha-grid-size-picker> ></ha-grid-size-picker>
<ha-settings-row>
<span slot="heading" data-for="full-width">
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.full_width"
)}
</span>
<span slot="description" data-for="full-width">
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.full_width_helper"
)}
</span>
<ha-switch
@change=${this._fullWidthChanged}
.checked=${value.columns === "full"}
name="full-width"
>
</ha-switch>
</ha-settings-row>
`} `}
`; `;
} }
@ -195,7 +217,7 @@ export class HuiCardLayoutEditor extends LitElement {
private _gridSizeChanged(ev: CustomEvent): void { private _gridSizeChanged(ev: CustomEvent): void {
ev.stopPropagation(); ev.stopPropagation();
const value = ev.detail.value; const value = ev.detail.value as CardGridSize;
const newConfig: LovelaceCardConfig = { const newConfig: LovelaceCardConfig = {
...this.config, ...this.config,
@ -229,6 +251,21 @@ export class HuiCardLayoutEditor extends LitElement {
fireEvent(this, "value-changed", { value: newConfig }); fireEvent(this, "value-changed", { value: newConfig });
} }
private _fullWidthChanged(ev): void {
ev.stopPropagation();
const value = ev.target.checked;
const newConfig: LovelaceCardConfig = {
...this.config,
layout_options: {
...this.config.layout_options,
grid_columns: value
? "full"
: (this._defaultLayoutOptions?.grid_min_columns ?? 1),
},
};
fireEvent(this, "value-changed", { value: newConfig });
}
static styles = [ static styles = [
haStyle, haStyle,
css` css`
@ -262,6 +299,13 @@ export class HuiCardLayoutEditor extends LitElement {
display: block; display: block;
margin: 16px 0; margin: 16px 0;
} }
ha-formfield {
display: flex;
align-items: center;
--mdc-typography-body2-font-size: 1em;
max-width: 250px;
margin: 16px auto;
}
`, `,
]; ];
} }

View File

@ -14,8 +14,8 @@ import type { HomeAssistant } from "../../../types";
import { HuiCard } from "../cards/hui-card"; import { HuiCard } from "../cards/hui-card";
import "../components/hui-card-edit-mode"; import "../components/hui-card-edit-mode";
import { moveCard } from "../editor/config-util"; import { moveCard } from "../editor/config-util";
import type { Lovelace, LovelaceLayoutOptions } from "../types"; import type { Lovelace } from "../types";
import { conditionalClamp } from "../../../common/number/clamp"; import { computeCardGridSize } from "../common/compute-card-grid-size";
const CARD_SORTABLE_OPTIONS: HaSortableOptions = { const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
delay: 100, delay: 100,
@ -24,43 +24,6 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
invertedSwapThreshold: 0.7, invertedSwapThreshold: 0.7,
} as HaSortableOptions; } as HaSortableOptions;
export const DEFAULT_GRID_OPTIONS = {
grid_columns: 4,
grid_rows: "auto",
} as const satisfies LovelaceLayoutOptions;
type GridSizeValue = {
rows?: number | "auto";
columns?: number;
};
export const computeSizeOnGrid = (
options: LovelaceLayoutOptions
): GridSizeValue => {
const rows =
typeof options.grid_rows === "number"
? conditionalClamp(
options.grid_rows,
options.grid_min_rows,
options.grid_max_rows
)
: DEFAULT_GRID_OPTIONS.grid_rows;
const columns =
typeof options.grid_columns === "number"
? conditionalClamp(
options.grid_columns,
options.grid_min_columns,
options.grid_max_columns
)
: DEFAULT_GRID_OPTIONS.grid_columns;
return {
rows,
columns,
};
};
export class GridSection extends LitElement implements LovelaceSectionElement { export class GridSection extends LitElement implements LovelaceSectionElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -134,16 +97,18 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
card.layout = "grid"; card.layout = "grid";
const layoutOptions = card.getLayoutOptions(); const layoutOptions = card.getLayoutOptions();
const { rows, columns } = computeSizeOnGrid(layoutOptions); const { rows, columns } = computeCardGridSize(layoutOptions);
return html` return html`
<div <div
style=${styleMap({ style=${styleMap({
"--column-size": columns, "--column-size":
"--row-size": rows, typeof columns === "number" ? columns : undefined,
"--row-size": typeof rows === "number" ? rows : undefined,
})} })}
class="card ${classMap({ class="card ${classMap({
"fit-rows": typeof layoutOptions?.grid_rows === "number", "fit-rows": typeof layoutOptions?.grid_rows === "number",
"full-width": columns === "full",
})}" })}"
> >
${editMode ${editMode
@ -268,8 +233,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
.card { .card {
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);
position: relative; position: relative;
grid-row: span var(--row-size); grid-row: span var(--row-size, 1);
grid-column: span var(--column-size); grid-column: span min(var(--column-size, 1), var(--column-count));
} }
.card.fit-rows { .card.fit-rows {
@ -280,6 +245,10 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
); );
} }
.card.full-width {
grid-column: 1 / -1;
}
.card:has(> *) { .card:has(> *) {
display: block; display: block;
} }

View File

@ -42,7 +42,7 @@ export interface LovelaceBadge extends HTMLElement {
} }
export type LovelaceLayoutOptions = { export type LovelaceLayoutOptions = {
grid_columns?: number; grid_columns?: number | "full";
grid_rows?: number | "auto"; grid_rows?: number | "auto";
grid_max_columns?: number; grid_max_columns?: number;
grid_min_columns?: number; grid_min_columns?: number;

View File

@ -5588,6 +5588,10 @@
"tab_layout": "Layout", "tab_layout": "Layout",
"visibility": { "visibility": {
"explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown." "explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown."
},
"layout": {
"full_width": "Full width card",
"full_width_helper": "Take up the full width of the section whatever its size"
} }
}, },
"edit_badge": { "edit_badge": {