diff --git a/src/components/ha-grid-size-picker.ts b/src/components/ha-grid-size-picker.ts index f62fc5c01d..30fdf1a9f9 100644 --- a/src/components/ha-grid-size-picker.ts +++ b/src/components/ha-grid-size-picker.ts @@ -1,24 +1,24 @@ import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "./ha-icon-button"; import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider"; +import "./ha-icon-button"; import { mdiRestore } from "@mdi/js"; +import { classMap } from "lit/directives/class-map"; import { styleMap } from "lit/directives/style-map"; import { fireEvent } from "../common/dom/fire_event"; -import { HomeAssistant } from "../types"; import { conditionalClamp } from "../common/number/clamp"; - -type GridSizeValue = { - rows?: number | "auto"; - columns?: number; -}; +import { + CardGridSize, + DEFAULT_GRID_SIZE, +} from "../panels/lovelace/common/compute-card-grid-size"; +import { HomeAssistant } from "../types"; @customElement("ha-grid-size-picker") export class HaGridSizeEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public value?: GridSizeValue; + @property({ attribute: false }) public value?: CardGridSize; @property({ attribute: false }) public rows = 8; @@ -34,7 +34,7 @@ export class HaGridSizeEditor extends LitElement { @property({ attribute: false }) public isDefault?: boolean; - @state() public _localValue?: GridSizeValue = undefined; + @state() public _localValue?: CardGridSize = { rows: 1, columns: 1 }; protected willUpdate(changedProperties) { if (changedProperties.has("value")) { @@ -49,6 +49,7 @@ export class HaGridSizeEditor extends LitElement { this.rowMin !== undefined && this.rowMin === this.rowMax; const autoHeight = this._localValue?.rows === "auto"; + const fullWidth = this._localValue?.columns === "full"; const rowMin = this.rowMin ?? 1; const rowMax = this.rowMax ?? this.rows; @@ -67,7 +68,7 @@ export class HaGridSizeEditor extends LitElement { .min=${columnMin} .max=${columnMax} .range=${this.columns} - .value=${columnValue} + .value=${fullWidth ? this.columns : columnValue} @value-changed=${this._valueChanged} @slider-moved=${this._sliderMoved} .disabled=${disabledColumns} @@ -104,12 +105,12 @@ export class HaGridSizeEditor extends LitElement { ` : nothing}
@@ -140,12 +141,21 @@ export class HaGridSizeEditor extends LitElement { const cell = ev.currentTarget as HTMLElement; const rows = Number(cell.getAttribute("data-row")); const columns = Number(cell.getAttribute("data-column")); - const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax); - const clampedColumn = conditionalClamp( + const clampedRow: CardGridSize["rows"] = conditionalClamp( + rows, + this.rowMin, + this.rowMax + ); + let clampedColumn: CardGridSize["columns"] = conditionalClamp( columns, this.columnMin, this.columnMax ); + + const currentSize = this.value ?? DEFAULT_GRID_SIZE; + if (currentSize.columns === "full" && clampedColumn === this.columns) { + clampedColumn = "full"; + } fireEvent(this, "value-changed", { value: { rows: clampedRow, columns: clampedColumn }, }); @@ -153,12 +163,23 @@ export class HaGridSizeEditor extends LitElement { private _valueChanged(ev) { ev.stopPropagation(); - const key = ev.currentTarget.id; - const newValue = { - ...this.value, - [key]: ev.detail.value, + const key = ev.currentTarget.id as "rows" | "columns"; + const currentSize = this.value ?? DEFAULT_GRID_SIZE; + let value = ev.detail.value as CardGridSize[typeof key]; + + 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) { @@ -173,11 +194,14 @@ export class HaGridSizeEditor extends LitElement { private _sliderMoved(ev) { ev.stopPropagation(); - const key = ev.currentTarget.id; - const value = ev.detail.value; + const key = ev.currentTarget.id as "rows" | "columns"; + const currentSize = this.value ?? DEFAULT_GRID_SIZE; + const value = ev.detail.value as CardGridSize[typeof key] | undefined; + if (value === undefined) return; + this._localValue = { - ...this.value, + ...currentSize, [key]: ev.detail.value, }; } @@ -231,10 +255,13 @@ export class HaGridSizeEditor extends LitElement { } .selected .cell { background-color: var(--primary-color); - grid-column: 1 / span var(--columns, 0); - grid-row: 1 / span var(--rows, 0); + grid-column: 1 / span min(var(--columns, 0), var(--total-columns)); + grid-row: 1 / span min(var(--rows, 0), var(--total-rows)); opacity: 0.5; } + .preview.full-width .selected .cell { + grid-column: 1 / -1; + } `, ]; } diff --git a/src/panels/lovelace/cards/hui-iframe-card.ts b/src/panels/lovelace/cards/hui-iframe-card.ts index cc2cdeade5..b70af9a7ab 100644 --- a/src/panels/lovelace/cards/hui-iframe-card.ts +++ b/src/panels/lovelace/cards/hui-iframe-card.ts @@ -114,7 +114,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard { public getLayoutOptions(): LovelaceLayoutOptions { return { - grid_columns: 4, + grid_columns: "full", grid_rows: 4, grid_min_rows: 2, }; diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index 17e6df279f..ae3bd52798 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -426,7 +426,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { public getLayoutOptions(): LovelaceLayoutOptions { return { - grid_columns: 4, + grid_columns: "full", grid_rows: 4, grid_min_columns: 2, grid_min_rows: 2, diff --git a/src/panels/lovelace/common/compute-card-grid-size.ts b/src/panels/lovelace/common/compute-card-grid-size.ts new file mode 100644 index 0000000000..c27b5a55ba --- /dev/null +++ b/src/panels/lovelace/common/compute-card-grid-size.ts @@ -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, + }; +}; diff --git a/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts index ac32343f75..d944d94609 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts @@ -1,6 +1,6 @@ import type { ActionDetail } from "@material/mwc-list"; 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 memoizeOne from "memoize-one"; 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 "../../../../components/ha-button"; import "../../../../components/ha-button-menu"; +import "../../../../components/ha-formfield"; import "../../../../components/ha-grid-size-picker"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-list-item"; @@ -21,7 +22,10 @@ import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; 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"; @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( (options?: LovelaceLayoutOptions) => @@ -63,7 +67,7 @@ export class HuiCardLayoutEditor extends LitElement { this._defaultLayoutOptions ); - const sizeValue = this._gridSizeValue(options); + const value = this._computeCardGridSize(options); return html`
@@ -128,7 +132,7 @@ export class HuiCardLayoutEditor extends LitElement { : html` + + + ${this.hass.localize( + "ui.panel.lovelace.editor.edit_card.layout.full_width" + )} + + + ${this.hass.localize( + "ui.panel.lovelace.editor.edit_card.layout.full_width_helper" + )} + + + + `} `; } @@ -195,7 +217,7 @@ export class HuiCardLayoutEditor extends LitElement { private _gridSizeChanged(ev: CustomEvent): void { ev.stopPropagation(); - const value = ev.detail.value; + const value = ev.detail.value as CardGridSize; const newConfig: LovelaceCardConfig = { ...this.config, @@ -229,6 +251,21 @@ export class HuiCardLayoutEditor extends LitElement { 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 = [ haStyle, css` @@ -262,6 +299,13 @@ export class HuiCardLayoutEditor extends LitElement { display: block; margin: 16px 0; } + ha-formfield { + display: flex; + align-items: center; + --mdc-typography-body2-font-size: 1em; + max-width: 250px; + margin: 16px auto; + } `, ]; } diff --git a/src/panels/lovelace/sections/hui-grid-section.ts b/src/panels/lovelace/sections/hui-grid-section.ts index bd67d01429..98cfe2187a 100644 --- a/src/panels/lovelace/sections/hui-grid-section.ts +++ b/src/panels/lovelace/sections/hui-grid-section.ts @@ -14,8 +14,8 @@ import type { HomeAssistant } from "../../../types"; import { HuiCard } from "../cards/hui-card"; import "../components/hui-card-edit-mode"; import { moveCard } from "../editor/config-util"; -import type { Lovelace, LovelaceLayoutOptions } from "../types"; -import { conditionalClamp } from "../../../common/number/clamp"; +import type { Lovelace } from "../types"; +import { computeCardGridSize } from "../common/compute-card-grid-size"; const CARD_SORTABLE_OPTIONS: HaSortableOptions = { delay: 100, @@ -24,43 +24,6 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = { invertedSwapThreshold: 0.7, } 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 { @property({ attribute: false }) public hass!: HomeAssistant; @@ -134,16 +97,18 @@ export class GridSection extends LitElement implements LovelaceSectionElement { card.layout = "grid"; const layoutOptions = card.getLayoutOptions(); - const { rows, columns } = computeSizeOnGrid(layoutOptions); + const { rows, columns } = computeCardGridSize(layoutOptions); return html`
${editMode @@ -268,8 +233,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement { .card { border-radius: var(--ha-card-border-radius, 12px); position: relative; - grid-row: span var(--row-size); - grid-column: span var(--column-size); + grid-row: span var(--row-size, 1); + grid-column: span min(var(--column-size, 1), var(--column-count)); } .card.fit-rows { @@ -280,6 +245,10 @@ export class GridSection extends LitElement implements LovelaceSectionElement { ); } + .card.full-width { + grid-column: 1 / -1; + } + .card:has(> *) { display: block; } diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index 92a2658e2e..ca9ae97654 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -42,7 +42,7 @@ export interface LovelaceBadge extends HTMLElement { } export type LovelaceLayoutOptions = { - grid_columns?: number; + grid_columns?: number | "full"; grid_rows?: number | "auto"; grid_max_columns?: number; grid_min_columns?: number; diff --git a/src/translations/en.json b/src/translations/en.json index 5467f70651..0ae75011f0 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5588,6 +5588,10 @@ "tab_layout": "Layout", "visibility": { "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": {