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

View File

@ -36,7 +36,11 @@ import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
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 { EntityCardConfig } from "./types";
@ -241,6 +245,15 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
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 {
return [
iconColorCSS,

View File

@ -22,7 +22,11 @@ import { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
import { HumidifierCardConfig } from "./types";
@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 {
return css`
:host {

View File

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

View File

@ -432,6 +432,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return {
grid_columns: 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 "../components/hui-marquee";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
import { MediaControlCardConfig } from "./types";
@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 {
return css`
ha-card {

View File

@ -76,6 +76,8 @@ class HuiSensorCard extends HuiEntityCard {
return {
grid_columns: 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 {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
@ -16,12 +16,12 @@ import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-state-icon";
import {
StatisticsMetaData,
fetchStatistic,
getDisplayUnit,
getStatisticLabel,
getStatisticMetadata,
isExternalStatistic,
StatisticsMetaData,
} from "../../../data/recorder";
import { HomeAssistant } from "../../../types";
import { computeCardSize } from "../common/compute-card-size";
@ -32,6 +32,7 @@ import {
LovelaceCard,
LovelaceCardEditor,
LovelaceHeaderFooter,
LovelaceLayoutOptions,
} from "../types";
import { HuiErrorCard } from "./hui-error-card";
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 });
}
public getLayoutOptions(): LovelaceLayoutOptions {
return {
grid_columns: 2,
grid_rows: 2,
grid_min_columns: 2,
grid_min_rows: 2,
};
}
static get styles(): CSSResultGroup {
return [
css`

View File

@ -22,7 +22,11 @@ import { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
import { ThermostatCardConfig } from "./types";
@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 {
return css`
:host {

View File

@ -122,17 +122,21 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}
public getLayoutOptions(): LovelaceLayoutOptions {
const options = {
grid_columns: 2,
grid_rows: 1,
};
const grid_columns = 2;
let grid_rows = 1;
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) {
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) {

View File

@ -38,7 +38,11 @@ import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
import type { WeatherForecastCardConfig } from "./types";
@customElement("hui-weather-forecast-card")
@ -421,6 +425,26 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
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 {
return [
weatherSVGStyles,

View File

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

View File

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

View File

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

View File

@ -43,6 +43,10 @@ export interface LovelaceBadge extends HTMLElement {
export type LovelaceLayoutOptions = {
grid_columns?: 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 {