Add min/max row/columns to resize card editor (#21244)

* Add min/max row/columns to resize card editor

* Add humidifier and thermostat card

* Removed unused condition

* Don't set max rows

* Add media card

* Add button card

* Use same rule if there is footer

* Don't show disabled cell

* Add min rows to sensor card

* Update sizes

* Update sizes

* Update sizes

* Add min rows to weather forecast card
This commit is contained in:
Paul Bottein 2024-07-02 21:19:29 +02:00 committed by GitHub
parent 9a2051a679
commit 094203f0b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 222 additions and 56 deletions

View File

@ -7,6 +7,7 @@ import { mdiRestore } from "@mdi/js";
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 { HomeAssistant } from "../types";
import { conditionalClamp } from "../common/number/clamp";
type GridSizeValue = { type GridSizeValue = {
rows?: number; rows?: number;
@ -42,6 +43,10 @@ export class HaGridSizeEditor extends LitElement {
} }
protected render() { protected render() {
const disabledColumns =
this.columnMin !== undefined && this.columnMin === this.columnMax;
const disabledRows =
this.rowMin !== undefined && this.rowMin === this.rowMax;
return html` return html`
<div class="grid"> <div class="grid">
<ha-grid-layout-slider <ha-grid-layout-slider
@ -55,6 +60,7 @@ export class HaGridSizeEditor extends LitElement {
.value=${this.value?.columns} .value=${this.value?.columns}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved} @slider-moved=${this._sliderMoved}
.disabled=${disabledColumns}
></ha-grid-layout-slider> ></ha-grid-layout-slider>
<ha-grid-layout-slider <ha-grid-layout-slider
aria-label=${this.hass.localize( aria-label=${this.hass.localize(
@ -68,6 +74,7 @@ export class HaGridSizeEditor extends LitElement {
.value=${this.value?.rows} .value=${this.value?.rows}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved} @slider-moved=${this._sliderMoved}
.disabled=${disabledRows}
></ha-grid-layout-slider> ></ha-grid-layout-slider>
${!this.isDefault ${!this.isDefault
? html` ? html`
@ -100,17 +107,11 @@ export class HaGridSizeEditor extends LitElement {
.map((_, index) => { .map((_, index) => {
const row = Math.floor(index / this.columns) + 1; const row = Math.floor(index / this.columns) + 1;
const column = (index % this.columns) + 1; const column = (index % this.columns) + 1;
const disabled =
(this.rowMin !== undefined && row < this.rowMin) ||
(this.rowMax !== undefined && row > this.rowMax) ||
(this.columnMin !== undefined && column < this.columnMin) ||
(this.columnMax !== undefined && column > this.columnMax);
return html` return html`
<div <div
class="cell" class="cell"
data-row=${row} data-row=${row}
data-column=${column} data-column=${column}
?disabled=${disabled}
@click=${this._cellClick} @click=${this._cellClick}
></div> ></div>
`; `;
@ -126,11 +127,16 @@ export class HaGridSizeEditor extends LitElement {
_cellClick(ev) { _cellClick(ev) {
const cell = ev.currentTarget as HTMLElement; const cell = ev.currentTarget as HTMLElement;
if (cell.getAttribute("disabled") !== null) return;
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 clampedColumn = conditionalClamp(
columns,
this.columnMin,
this.columnMax
);
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { rows, columns }, value: { rows: clampedRow, columns: clampedColumn },
}); });
} }

View File

@ -145,9 +145,16 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
this._config?.show_icon && this._config?.show_icon &&
(this._config?.show_name || this._config?.show_state) (this._config?.show_name || this._config?.show_state)
) { ) {
return { grid_rows: 2, grid_columns: 2 }; return {
grid_rows: 2,
grid_columns: 2,
grid_min_rows: 2,
};
} }
return { grid_rows: 1, grid_columns: 1 }; return {
grid_rows: 1,
grid_columns: 1,
};
} }
public setConfig(config: ButtonCardConfig): void { public setConfig(config: ButtonCardConfig): void {

View File

@ -36,7 +36,11 @@ import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed"; import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
import { createHeaderFooterElement } from "../create-element/create-header-footer-element"; import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
import { LovelaceCard, LovelaceHeaderFooter } from "../types"; import {
LovelaceCard,
LovelaceHeaderFooter,
LovelaceLayoutOptions,
} from "../types";
import { HuiErrorCard } from "./hui-error-card"; import { HuiErrorCard } from "./hui-error-card";
import { EntityCardConfig } from "./types"; import { EntityCardConfig } from "./types";
@ -241,6 +245,15 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
} }
public getLayoutOptions(): LovelaceLayoutOptions {
return {
grid_columns: 2,
grid_rows: 2,
grid_min_columns: 2,
grid_min_rows: 2,
};
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
iconColorCSS, iconColorCSS,

View File

@ -22,7 +22,11 @@ import { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features"; import "../card-features/hui-card-features";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types"; import {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
import { HumidifierCardConfig } from "./types"; import { HumidifierCardConfig } from "./types";
@customElement("hui-humidifier-card") @customElement("hui-humidifier-card")
@ -173,6 +177,24 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
`; `;
} }
public getLayoutOptions(): LovelaceLayoutOptions {
const grid_columns = 4;
let grid_rows = 5;
let grid_min_rows = 2;
const grid_min_columns = 2;
if (this._config?.features?.length) {
const featureHeight = Math.ceil((this._config.features.length * 2) / 3);
grid_rows += featureHeight;
grid_min_rows += featureHeight;
}
return {
grid_columns,
grid_rows,
grid_min_rows,
grid_min_columns,
};
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {

View File

@ -119,6 +119,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
return { return {
grid_columns: 4, grid_columns: 4,
grid_rows: 4, grid_rows: 4,
grid_min_rows: 2,
}; };
} }

View File

@ -432,6 +432,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return { return {
grid_columns: 4, grid_columns: 4,
grid_rows: 4, grid_rows: 4,
grid_min_columns: 2,
grid_min_rows: 2,
}; };
} }

View File

@ -40,7 +40,11 @@ import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed"; import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-marquee"; import "../components/hui-marquee";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types"; import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
import { MediaControlCardConfig } from "./types"; import { MediaControlCardConfig } from "./types";
@customElement("hui-media-control-card") @customElement("hui-media-control-card")
@ -582,6 +586,15 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
} }
} }
public getLayoutOptions(): LovelaceLayoutOptions {
return {
grid_columns: 4,
grid_min_columns: 2,
grid_rows: 3,
grid_min_rows: 3,
};
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-card { ha-card {

View File

@ -76,6 +76,8 @@ class HuiSensorCard extends HuiEntityCard {
return { return {
grid_columns: 2, grid_columns: 2,
grid_rows: 2, grid_rows: 2,
grid_min_columns: 2,
grid_min_rows: 2,
}; };
} }

View File

@ -1,10 +1,10 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
PropertyValues, PropertyValues,
css,
html,
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -16,12 +16,12 @@ import "../../../components/ha-alert";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
import { import {
StatisticsMetaData,
fetchStatistic, fetchStatistic,
getDisplayUnit, getDisplayUnit,
getStatisticLabel, getStatisticLabel,
getStatisticMetadata, getStatisticMetadata,
isExternalStatistic, isExternalStatistic,
StatisticsMetaData,
} from "../../../data/recorder"; } from "../../../data/recorder";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { computeCardSize } from "../common/compute-card-size"; import { computeCardSize } from "../common/compute-card-size";
@ -32,6 +32,7 @@ import {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
LovelaceHeaderFooter, LovelaceHeaderFooter,
LovelaceLayoutOptions,
} from "../types"; } from "../types";
import { HuiErrorCard } from "./hui-error-card"; import { HuiErrorCard } from "./hui-error-card";
import { EntityCardConfig, StatisticCardConfig } from "./types"; import { EntityCardConfig, StatisticCardConfig } from "./types";
@ -254,6 +255,15 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
} }
public getLayoutOptions(): LovelaceLayoutOptions {
return {
grid_columns: 2,
grid_rows: 2,
grid_min_columns: 2,
grid_min_rows: 2,
};
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
css` css`

View File

@ -22,7 +22,11 @@ import { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features"; import "../card-features/hui-card-features";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types"; import {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
import { ThermostatCardConfig } from "./types"; import { ThermostatCardConfig } from "./types";
@customElement("hui-thermostat-card") @customElement("hui-thermostat-card")
@ -165,6 +169,24 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
`; `;
} }
public getLayoutOptions(): LovelaceLayoutOptions {
const grid_columns = 4;
let grid_rows = 5;
let grid_min_rows = 2;
const grid_min_columns = 2;
if (this._config?.features?.length) {
const featureHeight = Math.ceil((this._config.features.length * 2) / 3);
grid_rows += featureHeight;
grid_min_rows += featureHeight;
}
return {
grid_columns,
grid_rows,
grid_min_rows,
grid_min_columns,
};
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {

View File

@ -122,17 +122,21 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getLayoutOptions(): LovelaceLayoutOptions {
const options = { const grid_columns = 2;
grid_columns: 2, let grid_rows = 1;
grid_rows: 1,
};
if (this._config?.features?.length) { if (this._config?.features?.length) {
options.grid_rows += Math.ceil((this._config.features.length * 2) / 3); const featureHeight = Math.ceil((this._config.features.length * 2) / 3);
grid_rows += featureHeight;
} }
if (this._config?.vertical) { if (this._config?.vertical) {
options.grid_rows++; grid_rows!++;
} }
return options; return {
grid_columns,
grid_rows,
grid_min_rows: grid_rows,
grid_min_columns: grid_columns,
};
} }
private _handleAction(ev: ActionHandlerEvent) { private _handleAction(ev: ActionHandlerEvent) {

View File

@ -38,7 +38,11 @@ import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action"; import { hasAction } from "../common/has-action";
import { hasConfigOrEntityChanged } from "../common/has-changed"; import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types"; import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
import type { WeatherForecastCardConfig } from "./types"; import type { WeatherForecastCardConfig } from "./types";
@customElement("hui-weather-forecast-card") @customElement("hui-weather-forecast-card")
@ -421,6 +425,26 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
return typeof item !== "undefined" && item !== null; return typeof item !== "undefined" && item !== null;
} }
public getLayoutOptions(): LovelaceLayoutOptions {
if (
this._config?.show_current !== false &&
this._config?.show_forecast !== false
) {
return {
grid_columns: 4,
grid_min_columns: 2,
grid_rows: 3,
grid_min_rows: 3,
};
}
return {
grid_columns: 4,
grid_min_columns: 2,
grid_rows: 2,
grid_min_rows: 1,
};
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
weatherSVGStyles, weatherSVGStyles,

View File

@ -255,15 +255,14 @@ export class HaGridLayoutSlider extends LitElement {
> >
<div id="slider" class="slider"> <div id="slider" class="slider">
<div class="track"> <div class="track">
<div class="background"> <div class="background"></div>
<div <div
class="active" class="active"
style=${styleMap({ style=${styleMap({
"--min": `${this.min / this._range}`, "--min": `${this.min / this._range}`,
"--max": `${1 - this.max / this._range}`, "--max": `${1 - this.max / this._range}`,
})} })}
></div> ></div>
</div>
</div> </div>
${this.value !== undefined ${this.value !== undefined
? html`<div class="handle"></div>` ? html`<div class="handle"></div>`
@ -323,11 +322,12 @@ export class HaGridLayoutSlider extends LitElement {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: var(--disabled-color); background: var(--disabled-color);
opacity: 0.5; opacity: 0.2;
} }
.active { .active {
position: absolute; position: absolute;
background: grey; background: grey;
opacity: 0.7;
top: 0; top: 0;
right: calc(var(--max) * 100%); right: calc(var(--max) * 100%);
bottom: 0; bottom: 0;
@ -375,6 +375,9 @@ export class HaGridLayoutSlider extends LitElement {
:host(:disabled) .slider { :host(:disabled) .slider {
cursor: not-allowed; cursor: not-allowed;
} }
:host(:disabled) .handle:after {
background: var(--disabled-color);
}
.pressed .handle { .pressed .handle {
transition: none; transition: none;
} }

View File

@ -19,7 +19,7 @@ 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 { DEFAULT_GRID_OPTIONS } from "../../sections/hui-grid-section"; import { computeSizeOnGrid } from "../../sections/hui-grid-section";
import { LovelaceLayoutOptions } from "../../types"; import { LovelaceLayoutOptions } from "../../types";
@customElement("hui-card-layout-editor") @customElement("hui-card-layout-editor")
@ -38,28 +38,29 @@ export class HuiCardLayoutEditor extends LitElement {
private _cardElement?: HuiCard; private _cardElement?: HuiCard;
private _gridSizeValue = memoizeOne( private _mergedOptions = memoizeOne(
( (
options?: LovelaceLayoutOptions, options?: LovelaceLayoutOptions,
defaultOptions?: LovelaceLayoutOptions defaultOptions?: LovelaceLayoutOptions
) => ({ ) => ({
rows: ...defaultOptions,
options?.grid_rows ?? ...options,
defaultOptions?.grid_rows ??
DEFAULT_GRID_OPTIONS.grid_rows,
columns:
options?.grid_columns ??
defaultOptions?.grid_columns ??
DEFAULT_GRID_OPTIONS.grid_columns,
}) })
); );
private _gridSizeValue = memoizeOne(computeSizeOnGrid);
private _isDefault = memoizeOne( private _isDefault = memoizeOne(
(options?: LovelaceLayoutOptions) => (options?: LovelaceLayoutOptions) =>
options?.grid_columns === undefined && options?.grid_rows === undefined options?.grid_columns === undefined && options?.grid_rows === undefined
); );
render() { render() {
const options = this._mergedOptions(
this.config.layout_options,
this._defaultLayoutOptions
);
return html` return html`
<div class="header"> <div class="header">
<p class="intro"> <p class="intro">
@ -123,12 +124,13 @@ export class HuiCardLayoutEditor extends LitElement {
: html` : html`
<ha-grid-size-picker <ha-grid-size-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this._gridSizeValue( .value=${this._gridSizeValue(options)}
this.config.layout_options,
this._defaultLayoutOptions
)}
.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}
.rowMax=${options.grid_max_rows}
.columnMin=${options.grid_min_columns}
.columnMax=${options.grid_max_columns}
></ha-grid-size-picker> ></ha-grid-size-picker>
`} `}
`; `;

View File

@ -15,6 +15,7 @@ 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, LovelaceLayoutOptions } from "../types";
import { conditionalClamp } from "../../../common/number/clamp";
const CARD_SORTABLE_OPTIONS: HaSortableOptions = { const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
delay: 100, delay: 100,
@ -23,9 +24,41 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
invertedSwapThreshold: 0.7, invertedSwapThreshold: 0.7,
} as HaSortableOptions; } as HaSortableOptions;
export const DEFAULT_GRID_OPTIONS: LovelaceLayoutOptions = { export const DEFAULT_GRID_OPTIONS = {
grid_columns: 4, grid_columns: 4,
grid_rows: 1, grid_rows: 1,
} as const satisfies LovelaceLayoutOptions;
type GridSizeValue = {
rows?: number;
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 {
@ -101,15 +134,13 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
card.layout = "grid"; card.layout = "grid";
const layoutOptions = card.getLayoutOptions(); const layoutOptions = card.getLayoutOptions();
const columnSize = const { rows, columns } = computeSizeOnGrid(layoutOptions);
layoutOptions.grid_columns ?? DEFAULT_GRID_OPTIONS.grid_columns;
const rowSize =
layoutOptions.grid_rows ?? DEFAULT_GRID_OPTIONS.grid_rows;
return html` return html`
<div <div
style=${styleMap({ style=${styleMap({
"--column-size": columnSize, "--column-size": columns,
"--row-size": rowSize, "--row-size": rows,
})} })}
class="card ${classMap({ class="card ${classMap({
"fit-rows": typeof layoutOptions?.grid_rows === "number", "fit-rows": typeof layoutOptions?.grid_rows === "number",

View File

@ -43,6 +43,10 @@ export interface LovelaceBadge extends HTMLElement {
export type LovelaceLayoutOptions = { export type LovelaceLayoutOptions = {
grid_columns?: number; grid_columns?: number;
grid_rows?: number; grid_rows?: number;
grid_max_columns?: number;
grid_min_columns?: number;
grid_min_rows?: number;
grid_max_rows?: number;
}; };
export interface LovelaceCard extends HTMLElement { export interface LovelaceCard extends HTMLElement {