Compare commits

...
5 changed files with 225 additions and 160 deletions

No files matched your search

+75 -57
View File
@@ -1,22 +1,27 @@
import { mdiRestore } from "@mdi/js";
import { mdiGrid, mdiGridLarge } from "@mdi/js";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { conditionalClamp } from "../common/number/clamp";
import type { CardGridSize } from "../panels/lovelace/common/compute-card-grid-size";
import { DEFAULT_GRID_SIZE } from "../panels/lovelace/common/compute-card-grid-size";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import "./ha-icon-button";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import "./ha-icon-button";
export const GRID_SIZE_PICKER_ROWS = 8;
@customElement("ha-grid-size-picker")
export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public value?: CardGridSize;
@property({ attribute: false }) public rows = 8;
@property({ attribute: false }) public autoRows?: number;
@property({ attribute: false }) public rows = GRID_SIZE_PICKER_ROWS;
@property({ attribute: false }) public columns = 12;
@@ -28,10 +33,10 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public columnMax?: number;
@property({ attribute: false }) public isDefault?: boolean;
@property({ attribute: false }) public step = 1;
@property({ attribute: false }) public precise = false;
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
@state()
@@ -44,6 +49,24 @@ export class HaGridSizeEditor extends LitElement {
}
}
private _columnsText(columns: CardGridSize["columns"]): string {
if (columns === "full") {
return this._localize("ui.components.grid-size-picker.full_width");
}
return this._localize("ui.components.grid-size-picker.columns_value", {
count: columns,
});
}
private _rowsText(rows: CardGridSize["rows"]): string {
if (rows === "auto") {
return this._localize("ui.components.grid-size-picker.fit_content");
}
return this._localize("ui.components.grid-size-picker.rows_value", {
count: rows,
});
}
protected render() {
const autoHeight = this._localValue?.rows === "auto";
const fullWidth = this._localValue?.columns === "full";
@@ -59,11 +82,23 @@ export class HaGridSizeEditor extends LitElement {
const columnMin = Math.ceil((this.columnMin ?? 1) / this.step) * this.step;
const columnMax =
Math.ceil((this.columnMax ?? this.columns) / this.step) * this.step;
const rowValue = autoHeight ? rowMin : this._localValue?.rows;
const autoRows = conditionalClamp(this.autoRows ?? rowMin, rowMin, rowMax);
const rowValue = autoHeight ? autoRows : this._localValue?.rows;
const columnValue = this._localValue?.columns;
return html`
<div class="grid">
<ha-icon-button
class="precise"
.label=${this._localize(
this.precise
? "ui.components.grid-size-picker.exit_precise_mode"
: "ui.components.grid-size-picker.enter_precise_mode"
)}
.path=${this.precise ? mdiGrid : mdiGridLarge}
.selected=${this.precise}
@click=${this._togglePrecise}
></ha-icon-button>
<ha-grid-layout-slider
aria-label=${this._localize("ui.components.grid-size-picker.columns")}
id="columns"
@@ -71,6 +106,11 @@ export class HaGridSizeEditor extends LitElement {
.max=${columnMax}
.range=${this.columns}
.value=${fullWidth ? this.columns : this.value?.columns}
.valueText=${
columnValue !== undefined
? this._columnsText(columnValue)
: undefined
}
.step=${this.step}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
@@ -85,30 +125,17 @@ export class HaGridSizeEditor extends LitElement {
.max=${rowMax}
.range=${this.rows}
vertical
.value=${autoHeight ? rowMin : this.value?.rows}
.value=${autoHeight ? autoRows : this.value?.rows}
.valueText=${
this._localValue?.rows !== undefined
? this._rowsText(this._localValue.rows)
: undefined
}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledRows}
tooltip-mode=${disabledRows ? "never" : "always"}
></ha-grid-layout-slider>
${
!this.isDefault
? html`
<ha-icon-button
@click=${this._reset}
class="reset"
.path=${mdiRestore}
label=${this._localize(
"ui.components.grid-size-picker.reset_default"
)}
title=${this._localize(
"ui.components.grid-size-picker.reset_default"
)}
>
</ha-icon-button>
`
: nothing
}
<div class="preview">
<table>
${Array(this.rows)
@@ -140,7 +167,7 @@ export class HaGridSizeEditor extends LitElement {
})}
</table>
<div
class="preview-card"
class=${classMap({ "preview-card": true, auto: autoHeight })}
style=${styleMap({
"--rows": rowValue,
"--columns": fullWidth ? this.columns : columnValue,
@@ -152,6 +179,10 @@ export class HaGridSizeEditor extends LitElement {
`;
}
private _togglePrecise() {
fireEvent(this, "precise-mode-changed", { value: !this.precise });
}
private _cellClick(ev) {
const cell = ev.currentTarget as HTMLElement;
const rows = Number(cell.getAttribute("data-row"));
@@ -179,32 +210,8 @@ export class HaGridSizeEditor extends LitElement {
private _valueChanged(ev) {
ev.stopPropagation();
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: newSize });
}
private _reset(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
rows: undefined,
columns: undefined,
},
});
const value = ev.detail.value as number;
fireEvent(this, "value-changed", { value: { [key]: value } });
}
private _sliderMoved(ev) {
@@ -226,22 +233,27 @@ export class HaGridSizeEditor extends LitElement {
.grid {
display: grid;
grid-template-areas:
"reset column-slider"
"precise column-slider"
"row-slider preview";
grid-template-rows: auto auto;
grid-template-columns: auto 1fr;
gap: var(--ha-space-2);
}
.precise {
grid-area: precise;
--ha-icon-button-size: 36px;
}
.precise[selected] {
border-radius: 50%;
background-color: var(--primary-color);
color: var(--text-primary-color);
}
#columns {
grid-area: column-slider;
}
#rows {
grid-area: row-slider;
}
.reset {
grid-area: reset;
--ha-icon-button-size: 36px;
}
.preview {
position: relative;
grid-area: preview;
@@ -275,6 +287,9 @@ export class HaGridSizeEditor extends LitElement {
width ease-in-out 180ms,
height ease-in-out 180ms;
}
.preview-card.auto {
mask-image: linear-gradient(to bottom, black 60%, transparent);
}
`,
];
}
@@ -283,4 +298,7 @@ declare global {
interface HTMLElementTagNameMap {
"ha-grid-size-picker": HaGridSizeEditor;
}
interface HASSDomEvents {
"precise-mode-changed": { value: boolean };
}
}
@@ -40,6 +40,18 @@ export interface CardGridSize {
export const isPreciseMode = (options: LovelaceGridOptions) =>
typeof options.columns === "number" && options.columns % 3 !== 0;
const CARD_SIZE_HEIGHT = 50;
const GRID_ROW_HEIGHT = 56;
const GRID_GAP = 8;
export const cardSizeToGridRows = (cardSize: number): number =>
Math.max(
1,
Math.ceil(
(cardSize * CARD_SIZE_HEIGHT + GRID_GAP) / (GRID_ROW_HEIGHT + GRID_GAP)
)
);
export const computeCardGridSize = (
options: LovelaceGridOptions
): CardGridSize => {
@@ -54,6 +54,9 @@ export class HaGridLayoutSlider extends LitElement {
@property({ type: Number })
public range?: number;
@property({ attribute: false })
public valueText?: string;
@state()
public pressed = false;
@@ -94,10 +97,13 @@ export class HaGridLayoutSlider extends LitElement {
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (changedProps.has("value")) {
if (changedProps.has("value") || changedProps.has("valueText")) {
const valuenow = this._steppedValue(this.value ?? 0);
this.setAttribute("aria-valuenow", valuenow.toString());
this.setAttribute("aria-valuetext", valuenow.toString());
this.setAttribute(
"aria-valuetext",
this.valueText ?? valuenow.toString()
);
}
if (changedProps.has("min")) {
this.setAttribute("aria-valuemin", this.min.toString());
@@ -1,20 +1,16 @@
import { mdiDotsVertical, mdiPlaylistEdit } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { conditionalClamp } from "../../../../common/number/clamp";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-grid-size-picker";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-svg-icon";
import { GRID_SIZE_PICKER_ROWS } from "../../../../components/ha-grid-size-picker";
import "../../../../components/ha-switch";
import "../../../../components/ha-yaml-editor";
import type { HaSwitch } from "../../../../components/ha-switch";
import "../../../../components/item/ha-row-item";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import {
@@ -26,6 +22,7 @@ import type { HomeAssistant } from "../../../../types";
import type { HuiCard } from "../../cards/hui-card";
import type { CardGridSize } from "../../common/compute-card-grid-size";
import {
cardSizeToGridRows,
computeCardGridSize,
DEFAULT_GRID_SIZE,
GRID_COLUMN_MULTIPLIER,
@@ -44,9 +41,9 @@ export class HuiCardLayoutEditor extends LitElement {
@state() private _defaultGridOptions?: LovelaceGridOptions;
@state() private _yamlMode = false;
@state() private _customSize = false;
@state() private _uiAvailable = true;
@state() private _contentRows?: number;
@state() private _preciseMode = false;
@@ -62,6 +59,19 @@ export class HuiCardLayoutEditor extends LitElement {
private _computeCardGridSize = memoizeOne(computeCardGridSize);
private _recommendedSize = memoizeOne(
(options: LovelaceGridOptions, defaultOptions?: LovelaceGridOptions) => {
const limits = { ...options };
delete limits.columns;
delete limits.rows;
return computeCardGridSize({
...DEFAULT_GRID_SIZE,
...defaultOptions,
...limits,
});
}
);
private _isDefault = memoizeOne(
(options?: LovelaceGridOptions) =>
options?.columns === undefined && options?.rows === undefined
@@ -86,6 +96,10 @@ export class HuiCardLayoutEditor extends LitElement {
const gridOptions = options;
const gridValue = this._computeCardGridSize(gridOptions);
const recommended = this._recommendedSize(
configOptions,
this._defaultGridOptions
);
const columnSpan =
this.sectionConfig.column_span ?? DEFAULT_SECTION_COLUMN_SPAN;
@@ -104,44 +118,33 @@ export class HuiCardLayoutEditor extends LitElement {
`
: nothing
}
<div class="header">
<ha-dropdown
slot="icons"
@wa-select=${this._handleAction}
placement="bottom-end"
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.recommended_size"
)}</span
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
>
</ha-icon-button>
<ha-dropdown-item value="toggle_yaml" .disabled=${!this._uiAvailable}>
${this.hass.localize(
`ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-dropdown-item>
</ha-dropdown>
</div>
<span slot="supporting-text">${this._sizeText(recommended)}</span>
<ha-switch
slot="end"
@change=${this._recommendedSizeChanged}
.checked=${!this._customSize}
name="recommended-size"
></ha-switch>
</ha-row-item>
${
this._yamlMode
this._customSize
? html`
<ha-yaml-editor
.defaultValue=${configOptions}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>
`
: html`
<ha-grid-size-picker
style=${styleMap({
"max-width": `${(this.sectionConfig.column_span ?? DEFAULT_SECTION_COLUMN_SPAN) * 250 + 40}px`,
})}
.columns=${gridTotalColumns}
.value=${gridValue}
.isDefault=${this._isDefault(configOptions)}
.autoRows=${this._contentRows}
.precise=${this._preciseMode}
@value-changed=${this._gridSizeChanged}
@precise-mode-changed=${this._preciseModeChanged}
.rowMin=${gridOptions.min_rows}
.rowMax=${gridOptions.max_rows}
.columnMin=${gridOptions.min_columns}
@@ -151,12 +154,12 @@ export class HuiCardLayoutEditor extends LitElement {
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.auto_height"
"ui.panel.lovelace.editor.edit_card.layout.fit_content"
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.auto_height_helper"
"ui.panel.lovelace.editor.edit_card.layout.fit_content_helper"
)}</span
>
<ha-switch
@@ -184,29 +187,40 @@ export class HuiCardLayoutEditor extends LitElement {
name="full-width"
></ha-switch>
</ha-row-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.precise_mode"
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.precise_mode_helper"
)}</span
>
<ha-switch
slot="end"
@change=${this._preciseModeChanged}
.checked=${this._preciseMode}
name="precise-mode"
></ha-switch>
</ha-row-item>
`
: nothing
}
`;
}
private _widthText(columns: CardGridSize["columns"]): string {
if (columns === "full") {
return this.hass.localize("ui.components.grid-size-picker.full_width");
}
return this.hass.localize("ui.components.grid-size-picker.columns_value", {
count: columns,
});
}
private _heightText(rows: CardGridSize["rows"]): string {
if (rows === "auto") {
return this.hass.localize("ui.components.grid-size-picker.fit_content");
}
return this.hass.localize("ui.components.grid-size-picker.rows_value", {
count: rows,
});
}
private _sizeText(size: CardGridSize): string {
return this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.size",
{
width: this._widthText(size.columns),
height: this._heightText(size.rows),
}
);
}
protected firstUpdated(changedProps: PropertyValues<this>): void {
super.firstUpdated(changedProps);
try {
@@ -232,6 +246,19 @@ export class HuiCardLayoutEditor extends LitElement {
return;
}
this._defaultGridOptions = this._cardElement.getElementGridOptions();
this._updateContentRows(this._cardElement);
}
private async _updateContentRows(cardElement: HuiCard) {
const config = this.config;
const cardSize = await cardElement.getCardSize();
if (config !== this.config) {
return;
}
this._contentRows = Math.min(
cardSizeToGridRows(cardSize),
GRID_SIZE_PICKER_ROWS
);
}
protected updated(changedProps: PropertyValues<this>): void {
@@ -249,6 +276,10 @@ export class HuiCardLayoutEditor extends LitElement {
protected willUpdate(changedProps: PropertyValues<this>): void {
super.willUpdate(changedProps);
if (changedProps.has("config")) {
if (!this._isDefault(this._configGridOptions(this.config))) {
this._customSize = true;
}
const options = this.config.grid_options;
// Reset precise mode when grid options config is reset
@@ -267,36 +298,36 @@ export class HuiCardLayoutEditor extends LitElement {
}
}
private async _handleAction(ev: HaDropdownSelectEvent) {
const action = ev.detail.item.value;
if (action === "toggle_yaml") {
this._yamlMode = !this._yamlMode;
private _recommendedSizeChanged(
ev: HASSDomCurrentTargetEvent<HaSwitch>
): void {
ev.stopPropagation();
this._customSize = !ev.currentTarget.checked;
if (this._customSize) {
return;
}
this._updateGridOptions({
...this.config.grid_options,
columns: undefined,
rows: undefined,
});
}
private _gridSizeChanged(ev: CustomEvent): void {
ev.stopPropagation();
const value = ev.detail.value as CardGridSize;
const value = ev.detail.value as Partial<CardGridSize>;
this._updateGridOptions({
...this.config.grid_options,
columns: value.columns,
rows: value.rows,
...this._configGridOptions(this.config),
...value,
});
}
private _yamlChanged(ev: CustomEvent): void {
ev.stopPropagation();
const options = ev.detail.value as LovelaceGridOptions;
this._updateGridOptions(options);
}
private _fullWidthChanged(ev): void {
ev.stopPropagation();
const checked = ev.target.checked;
let columns: number | "full" | undefined;
let columns: number | "full";
const defaultGridOptions = {
...DEFAULT_GRID_SIZE,
...this._defaultGridOptions,
@@ -310,7 +341,7 @@ export class HuiCardLayoutEditor extends LitElement {
const gridTotalColumns = 12 * columnSpan;
columns = defaultGridOptions.max_columns ?? gridTotalColumns;
} else {
columns = undefined;
columns = defaultGridOptions.columns;
}
this._updateGridOptions({
@@ -323,7 +354,7 @@ export class HuiCardLayoutEditor extends LitElement {
ev.stopPropagation();
const checked = ev.target.checked;
let rows: number | "auto" | undefined;
let rows: number | "auto";
const defaultGridOptions = {
...DEFAULT_GRID_SIZE,
...this._defaultGridOptions,
@@ -332,9 +363,13 @@ export class HuiCardLayoutEditor extends LitElement {
rows = "auto";
} else if (defaultGridOptions.rows === "auto") {
// Default is auto height, so we need to set a specific value
rows = defaultGridOptions.min_rows ?? 1;
rows = conditionalClamp(
this._contentRows ?? 1,
defaultGridOptions.min_rows,
defaultGridOptions.max_rows
);
} else {
rows = undefined;
rows = defaultGridOptions.rows;
}
this._updateGridOptions({
@@ -345,7 +380,7 @@ export class HuiCardLayoutEditor extends LitElement {
private _preciseModeChanged(ev): void {
ev.stopPropagation();
this._preciseMode = ev.target.checked;
this._preciseMode = ev.detail.value;
if (this._preciseMode) return;
const columns = this.config.grid_options?.columns;
@@ -386,29 +421,18 @@ export class HuiCardLayoutEditor extends LitElement {
static styles = [
haStyle,
css`
.header {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-end;
}
.header ha-dropdown {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
margin-top: -8px;
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
ha-row-item::part(supporting-text) {
white-space: normal;
}
ha-grid-size-picker {
display: block;
margin: 16px auto;
direction: ltr;
}
ha-yaml-editor {
display: block;
margin: 16px 0;
}
`,
];
}
+13 -8
View File
@@ -1190,9 +1190,14 @@
}
},
"grid-size-picker": {
"reset_default": "Reset to default size",
"columns": "Number of columns",
"rows": "Number of rows"
"columns": "Width",
"rows": "Height",
"columns_value": "{count, plural, one {# column} other {# columns}}",
"rows_value": "{count, plural, one {# row} other {# rows}}",
"full_width": "Full width",
"fit_content": "Fit content",
"enter_precise_mode": "Enter precise mode",
"exit_precise_mode": "Exit precise mode"
},
"relative_time": {
"never": "Never"
@@ -10215,12 +10220,12 @@
"tab_layout": "Layout",
"paste_condition": "Paste condition",
"layout": {
"recommended_size": "Use recommended size",
"size": "{width} × {height}",
"full_width": "Full width",
"full_width_helper": "Take up the full width of the section whatever its size",
"auto_height": "Auto height",
"auto_height_helper": "Adjust the card height based on its content",
"precise_mode": "Precise mode",
"precise_mode_helper": "Change the card width with more precision",
"full_width_helper": "Always fill the section, whatever its width",
"fit_content": "Fit height to content",
"fit_content_helper": "Make the card as tall as its content",
"no_grid_support": "This card does not fully support resizing yet and may not display correctly with custom sizes."
}
},