mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 16:56:35 +00:00
Resize card editor (#21115)
This commit is contained in:
parent
6a3041988a
commit
321a085c0e
1
src/common/dom/prevent_default.ts
Normal file
1
src/common/dom/prevent_default.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const preventDefault = (ev) => ev.preventDefault();
|
233
src/components/ha-grid-size-picker.ts
Normal file
233
src/components/ha-grid-size-picker.ts
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
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 { mdiRestore } from "@mdi/js";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
type GridSizeValue = {
|
||||||
|
rows?: number;
|
||||||
|
columns?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@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 rows = 6;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public columns = 4;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public rowMin?: number;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public rowMax?: number;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public columnMin?: number;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public columnMax?: number;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public isDefault?: boolean;
|
||||||
|
|
||||||
|
@state() public _localValue?: GridSizeValue = undefined;
|
||||||
|
|
||||||
|
protected willUpdate(changedProperties) {
|
||||||
|
if (changedProperties.has("value")) {
|
||||||
|
this._localValue = this.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<div class="grid">
|
||||||
|
<ha-grid-layout-slider
|
||||||
|
aria-label=${this.hass.localize(
|
||||||
|
"ui.components.grid-size-picker.columns"
|
||||||
|
)}
|
||||||
|
id="columns"
|
||||||
|
.min=${this.columnMin ?? 1}
|
||||||
|
.max=${this.columnMax ?? this.columns}
|
||||||
|
.range=${this.columns}
|
||||||
|
.value=${this.value?.columns}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
@slider-moved=${this._sliderMoved}
|
||||||
|
></ha-grid-layout-slider>
|
||||||
|
<ha-grid-layout-slider
|
||||||
|
aria-label=${this.hass.localize(
|
||||||
|
"ui.components.grid-size-picker.rows"
|
||||||
|
)}
|
||||||
|
id="rows"
|
||||||
|
.min=${this.rowMin ?? 1}
|
||||||
|
.max=${this.rowMax ?? this.rows}
|
||||||
|
.range=${this.rows}
|
||||||
|
vertical
|
||||||
|
.value=${this.value?.rows}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
@slider-moved=${this._sliderMoved}
|
||||||
|
></ha-grid-layout-slider>
|
||||||
|
${!this.isDefault
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
@click=${this._reset}
|
||||||
|
class="reset"
|
||||||
|
.path=${mdiRestore}
|
||||||
|
label=${this.hass.localize(
|
||||||
|
"ui.components.grid-size-picker.reset_default"
|
||||||
|
)}
|
||||||
|
title=${this.hass.localize(
|
||||||
|
"ui.components.grid-size-picker.reset_default"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<div
|
||||||
|
class="preview"
|
||||||
|
style=${styleMap({
|
||||||
|
"--total-rows": this.rows,
|
||||||
|
"--total-columns": this.columns,
|
||||||
|
"--rows": this._localValue?.rows,
|
||||||
|
"--columns": this._localValue?.columns,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
${Array(this.rows * this.columns)
|
||||||
|
.fill(0)
|
||||||
|
.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>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="selected">
|
||||||
|
<div class="cell"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_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"));
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { rows, columns },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const key = ev.currentTarget.id;
|
||||||
|
const newValue = {
|
||||||
|
...this.value,
|
||||||
|
[key]: ev.detail.value,
|
||||||
|
};
|
||||||
|
fireEvent(this, "value-changed", { value: newValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _reset(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
rows: undefined,
|
||||||
|
columns: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _sliderMoved(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const key = ev.currentTarget.id;
|
||||||
|
const value = ev.detail.value;
|
||||||
|
if (value === undefined) return;
|
||||||
|
this._localValue = {
|
||||||
|
...this.value,
|
||||||
|
[key]: ev.detail.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
css`
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"reset column-slider"
|
||||||
|
"row-slider preview";
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
#columns {
|
||||||
|
grid-area: column-slider;
|
||||||
|
}
|
||||||
|
#rows {
|
||||||
|
grid-area: row-slider;
|
||||||
|
}
|
||||||
|
.reset {
|
||||||
|
grid-area: reset;
|
||||||
|
}
|
||||||
|
.preview {
|
||||||
|
position: relative;
|
||||||
|
grid-area: preview;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
.preview > div {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--total-columns), 1fr);
|
||||||
|
grid-template-rows: repeat(var(--total-rows), 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.preview .cell {
|
||||||
|
background-color: var(--disabled-color);
|
||||||
|
grid-column: span 1;
|
||||||
|
grid-row: span 1;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0.2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.preview .cell[disabled] {
|
||||||
|
opacity: 0.05;
|
||||||
|
cursor: initial;
|
||||||
|
}
|
||||||
|
.selected {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.selected .cell {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
grid-column: 1 / span var(--columns, 0);
|
||||||
|
grid-row: 1 / span var(--rows, 0);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-grid-size-picker": HaGridSizeEditor;
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,7 @@ export interface LovelaceViewElement extends HTMLElement {
|
|||||||
export interface LovelaceSectionElement extends HTMLElement {
|
export interface LovelaceSectionElement extends HTMLElement {
|
||||||
hass?: HomeAssistant;
|
hass?: HomeAssistant;
|
||||||
lovelace?: Lovelace;
|
lovelace?: Lovelace;
|
||||||
|
preview?: boolean;
|
||||||
viewIndex?: number;
|
viewIndex?: number;
|
||||||
index?: number;
|
index?: number;
|
||||||
cards?: HuiCard[];
|
cards?: HuiCard[];
|
||||||
|
@ -86,6 +86,10 @@ export class HuiCard extends ReactiveElement {
|
|||||||
return configOptions;
|
return configOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getElementLayoutOptions(): LovelaceLayoutOptions {
|
||||||
|
return this._element?.getLayoutOptions?.() ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
private _createElement(config: LovelaceCardConfig) {
|
private _createElement(config: LovelaceCardConfig) {
|
||||||
const element = createCardElement(config);
|
const element = createCardElement(config);
|
||||||
element.hass = this.hass;
|
element.hass = this.hass;
|
||||||
@ -155,7 +159,7 @@ export class HuiCard extends ReactiveElement {
|
|||||||
protected willUpdate(
|
protected willUpdate(
|
||||||
changedProps: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
changedProps: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||||
): void {
|
): void {
|
||||||
if (changedProps.has("hass") || changedProps.has("lovelace")) {
|
if (changedProps.has("hass") || changedProps.has("preview")) {
|
||||||
this._updateVisibility();
|
this._updateVisibility();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
389
src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts
Normal file
389
src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
|
||||||
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, query, 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";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"slider-moved": { value?: number };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const A11Y_KEY_CODES = new Set([
|
||||||
|
"ArrowRight",
|
||||||
|
"ArrowUp",
|
||||||
|
"ArrowLeft",
|
||||||
|
"ArrowDown",
|
||||||
|
"PageUp",
|
||||||
|
"PageDown",
|
||||||
|
"Home",
|
||||||
|
"End",
|
||||||
|
]);
|
||||||
|
|
||||||
|
@customElement("ha-grid-layout-slider")
|
||||||
|
export class HaGridLayoutSlider extends LitElement {
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public vertical = false;
|
||||||
|
|
||||||
|
@property({ attribute: "touch-action" })
|
||||||
|
public touchAction?: string;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public value?: number;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public step = 1;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public min = 1;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public max = 4;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public range?: number;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public pressed = false;
|
||||||
|
|
||||||
|
private _mc?: HammerManager;
|
||||||
|
|
||||||
|
private get _range() {
|
||||||
|
return this.range ?? this.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueToPercentage(value: number) {
|
||||||
|
const percentage = this._boundedValue(value) / this._range;
|
||||||
|
return percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _percentageToValue(percentage: number) {
|
||||||
|
return this._range * percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _steppedValue(value: number) {
|
||||||
|
return Math.round(value / this.step) * this.step;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _boundedValue(value: number) {
|
||||||
|
return Math.min(Math.max(value, this.min), this.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
this.setupListeners();
|
||||||
|
this.setAttribute("role", "slider");
|
||||||
|
if (!this.hasAttribute("tabindex")) {
|
||||||
|
this.setAttribute("tabindex", "0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has("value")) {
|
||||||
|
const valuenow = this._steppedValue(this.value ?? 0);
|
||||||
|
this.setAttribute("aria-valuenow", valuenow.toString());
|
||||||
|
this.setAttribute("aria-valuetext", valuenow.toString());
|
||||||
|
}
|
||||||
|
if (changedProps.has("min")) {
|
||||||
|
this.setAttribute("aria-valuemin", this.min.toString());
|
||||||
|
}
|
||||||
|
if (changedProps.has("max")) {
|
||||||
|
this.setAttribute("aria-valuemax", this.max.toString());
|
||||||
|
}
|
||||||
|
if (changedProps.has("vertical")) {
|
||||||
|
const orientation = this.vertical ? "vertical" : "horizontal";
|
||||||
|
this.setAttribute("aria-orientation", orientation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.destroyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@query("#slider")
|
||||||
|
private slider;
|
||||||
|
|
||||||
|
setupListeners() {
|
||||||
|
if (this.slider && !this._mc) {
|
||||||
|
this._mc = new Manager(this.slider, {
|
||||||
|
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
|
||||||
|
});
|
||||||
|
this._mc.add(
|
||||||
|
new Pan({
|
||||||
|
threshold: 10,
|
||||||
|
direction: DIRECTION_ALL,
|
||||||
|
enable: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this._mc.add(new Tap({ event: "singletap" }));
|
||||||
|
|
||||||
|
let savedValue;
|
||||||
|
this._mc.on("panstart", () => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this.pressed = true;
|
||||||
|
savedValue = this.value;
|
||||||
|
});
|
||||||
|
this._mc.on("pancancel", () => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this.pressed = false;
|
||||||
|
this.value = savedValue;
|
||||||
|
});
|
||||||
|
this._mc.on("panmove", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
const percentage = this._getPercentageFromEvent(e);
|
||||||
|
this.value = this._percentageToValue(percentage);
|
||||||
|
const value = this._steppedValue(this._boundedValue(this.value));
|
||||||
|
fireEvent(this, "slider-moved", { value });
|
||||||
|
});
|
||||||
|
this._mc.on("panend", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this.pressed = false;
|
||||||
|
const percentage = this._getPercentageFromEvent(e);
|
||||||
|
const value = this._percentageToValue(percentage);
|
||||||
|
this.value = this._steppedValue(this._boundedValue(value));
|
||||||
|
fireEvent(this, "slider-moved", { value: undefined });
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
});
|
||||||
|
|
||||||
|
this._mc.on("singletap", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
const percentage = this._getPercentageFromEvent(e);
|
||||||
|
const value = this._percentageToValue(percentage);
|
||||||
|
this.value = this._steppedValue(this._boundedValue(value));
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addEventListener("keydown", this._handleKeyDown);
|
||||||
|
this.addEventListener("keyup", this._handleKeyUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyListeners() {
|
||||||
|
if (this._mc) {
|
||||||
|
this._mc.destroy();
|
||||||
|
this._mc = undefined;
|
||||||
|
}
|
||||||
|
this.removeEventListener("keydown", this._handleKeyDown);
|
||||||
|
this.removeEventListener("keyup", this._handleKeyUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _tenPercentStep() {
|
||||||
|
return Math.max(this.step, (this.max - this.min) / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
switch (e.code) {
|
||||||
|
case "ArrowRight":
|
||||||
|
case "ArrowUp":
|
||||||
|
this.value = this._boundedValue((this.value ?? 0) + this.step);
|
||||||
|
break;
|
||||||
|
case "ArrowLeft":
|
||||||
|
case "ArrowDown":
|
||||||
|
this.value = this._boundedValue((this.value ?? 0) - this.step);
|
||||||
|
break;
|
||||||
|
case "PageUp":
|
||||||
|
this.value = this._steppedValue(
|
||||||
|
this._boundedValue((this.value ?? 0) + this._tenPercentStep)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "PageDown":
|
||||||
|
this.value = this._steppedValue(
|
||||||
|
this._boundedValue((this.value ?? 0) - this._tenPercentStep)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
this.value = this.min;
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
this.value = this.max;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fireEvent(this, "slider-moved", { value: this.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleKeyUp(e: KeyboardEvent) {
|
||||||
|
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getPercentageFromEvent = (e: HammerInput) => {
|
||||||
|
if (this.vertical) {
|
||||||
|
const y = e.center.y;
|
||||||
|
const offset = e.target.getBoundingClientRect().top;
|
||||||
|
const total = e.target.clientHeight;
|
||||||
|
return Math.max(Math.min(1, (y - offset) / total), 0);
|
||||||
|
}
|
||||||
|
const x = e.center.x;
|
||||||
|
const offset = e.target.getBoundingClientRect().left;
|
||||||
|
const total = e.target.clientWidth;
|
||||||
|
return Math.max(Math.min(1, (x - offset) / total), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="container${classMap({
|
||||||
|
pressed: this.pressed,
|
||||||
|
})}"
|
||||||
|
style=${styleMap({
|
||||||
|
"--value": `${this._valueToPercentage(this.value ?? 0)}`,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
${this.value !== undefined
|
||||||
|
? html`<div class="handle"></div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
--grid-layout-slider: 48px;
|
||||||
|
height: var(--grid-layout-slider);
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
transition: box-shadow 180ms ease-in-out;
|
||||||
|
}
|
||||||
|
:host(:focus-visible) {
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-color);
|
||||||
|
}
|
||||||
|
:host([vertical]) {
|
||||||
|
width: var(--grid-layout-slider);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.slider {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
transform: translateZ(0);
|
||||||
|
overflow: visible;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.slider * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.track {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: auto;
|
||||||
|
height: 16px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
:host([vertical]) .track {
|
||||||
|
width: 16px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--disabled-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
position: absolute;
|
||||||
|
background: grey;
|
||||||
|
top: 0;
|
||||||
|
right: calc(var(--max) * 100%);
|
||||||
|
bottom: 0;
|
||||||
|
left: calc(var(--min) * 100%);
|
||||||
|
}
|
||||||
|
:host([vertical]) .active {
|
||||||
|
top: calc(var(--min) * 100%);
|
||||||
|
right: 0;
|
||||||
|
bottom: calc(var(--max) * 100%);
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 16px;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
background: var(--card-background-color);
|
||||||
|
left: calc(var(--value, 0%) * 100%);
|
||||||
|
transition:
|
||||||
|
left 180ms ease-in-out,
|
||||||
|
top 180ms ease-in-out;
|
||||||
|
}
|
||||||
|
:host([vertical]) .handle {
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
left: 0;
|
||||||
|
top: calc(var(--value, 0%) * 100%);
|
||||||
|
height: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.handle::after {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
background: grey;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
:host([vertical]) .handle::after {
|
||||||
|
height: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
:host(:disabled) .slider {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.pressed .handle {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-grid-layout-slider": HaGridLayoutSlider;
|
||||||
|
}
|
||||||
|
}
|
@ -6,17 +6,21 @@ import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
|||||||
import { getCardElementClass } from "../../create-element/create-card-element";
|
import { getCardElementClass } from "../../create-element/create-card-element";
|
||||||
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
|
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
|
||||||
import { HuiElementEditor } from "../hui-element-editor";
|
import { HuiElementEditor } from "../hui-element-editor";
|
||||||
|
import "./hui-card-layout-editor";
|
||||||
import "./hui-card-visibility-editor";
|
import "./hui-card-visibility-editor";
|
||||||
|
|
||||||
const TABS = ["config", "visibility"] as const;
|
type Tab = "config" | "visibility" | "layout";
|
||||||
|
|
||||||
@customElement("hui-card-element-editor")
|
@customElement("hui-card-element-editor")
|
||||||
export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
||||||
@state() private _curTab: (typeof TABS)[number] = TABS[0];
|
@state() private _curTab: Tab = "config";
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "show-visibility-tab" })
|
@property({ type: Boolean, attribute: "show-visibility-tab" })
|
||||||
public showVisibilityTab = false;
|
public showVisibilityTab = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "show-layout-tab" })
|
||||||
|
public showLayoutTab = false;
|
||||||
|
|
||||||
protected async getConfigElement(): Promise<LovelaceCardEditor | undefined> {
|
protected async getConfigElement(): Promise<LovelaceCardEditor | undefined> {
|
||||||
const elClass = await getCardElementClass(this.configElementType!);
|
const elClass = await getCardElementClass(this.configElementType!);
|
||||||
|
|
||||||
@ -52,7 +56,11 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected renderConfigElement(): TemplateResult {
|
protected renderConfigElement(): TemplateResult {
|
||||||
if (!this.showVisibilityTab) return super.renderConfigElement();
|
const displayedTabs: Tab[] = ["config"];
|
||||||
|
if (this.showVisibilityTab) displayedTabs.push("visibility");
|
||||||
|
if (this.showLayoutTab) displayedTabs.push("layout");
|
||||||
|
|
||||||
|
if (displayedTabs.length === 1) return super.renderConfigElement();
|
||||||
|
|
||||||
let content: TemplateResult<1> | typeof nothing = nothing;
|
let content: TemplateResult<1> | typeof nothing = nothing;
|
||||||
|
|
||||||
@ -69,19 +77,28 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
|||||||
></hui-card-visibility-editor>
|
></hui-card-visibility-editor>
|
||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
|
case "layout":
|
||||||
|
content = html`
|
||||||
|
<hui-card-layout-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.config=${this.value}
|
||||||
|
@value-changed=${this._configChanged}
|
||||||
|
>
|
||||||
|
</hui-card-layout-editor>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<paper-tabs
|
<paper-tabs
|
||||||
scrollable
|
scrollable
|
||||||
hide-scroll-buttons
|
hide-scroll-buttons
|
||||||
.selected=${TABS.indexOf(this._curTab)}
|
.selected=${displayedTabs.indexOf(this._curTab)}
|
||||||
@selected-item-changed=${this._handleTabSelected}
|
@selected-item-changed=${this._handleTabSelected}
|
||||||
>
|
>
|
||||||
${TABS.map(
|
${displayedTabs.map(
|
||||||
(tab, index) => html`
|
(tab, index) => html`
|
||||||
<paper-tab id=${tab} .dialogInitialFocus=${index === 0}>
|
<paper-tab id=${tab} .dialogInitialFocus=${index === 0}>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
`ui.panel.lovelace.editor.edit_card.tab-${tab}`
|
`ui.panel.lovelace.editor.edit_card.tab_${tab}`
|
||||||
)}
|
)}
|
||||||
</paper-tab>
|
</paper-tab>
|
||||||
`
|
`
|
||||||
|
266
src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts
Normal file
266
src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
import type { ActionDetail } from "@material/mwc-list";
|
||||||
|
import { mdiCheck, mdiDotsVertical } from "@mdi/js";
|
||||||
|
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
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-grid-size-picker";
|
||||||
|
import "../../../../components/ha-icon-button";
|
||||||
|
import "../../../../components/ha-list-item";
|
||||||
|
import "../../../../components/ha-slider";
|
||||||
|
import "../../../../components/ha-svg-icon";
|
||||||
|
import "../../../../components/ha-yaml-editor";
|
||||||
|
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||||
|
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 { LovelaceLayoutOptions } from "../../types";
|
||||||
|
|
||||||
|
@customElement("hui-card-layout-editor")
|
||||||
|
export class HuiCardLayoutEditor extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public config!: LovelaceCardConfig;
|
||||||
|
|
||||||
|
@state() _defaultLayoutOptions?: LovelaceLayoutOptions;
|
||||||
|
|
||||||
|
@state() public _yamlMode = false;
|
||||||
|
|
||||||
|
@state() public _uiAvailable = true;
|
||||||
|
|
||||||
|
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||||
|
|
||||||
|
private _cardElement?: HuiCard;
|
||||||
|
|
||||||
|
private _gridSizeValue = 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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
private _isDefault = memoizeOne(
|
||||||
|
(options?: LovelaceLayoutOptions) =>
|
||||||
|
options?.grid_columns === undefined && options?.grid_rows === undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="header">
|
||||||
|
<p class="intro">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.lovelace.editor.edit_card.layout.explanation`
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<ha-button-menu
|
||||||
|
slot="icons"
|
||||||
|
@action=${this._handleAction}
|
||||||
|
@click=${preventDefault}
|
||||||
|
@closed=${stopPropagation}
|
||||||
|
fixed
|
||||||
|
.corner=${"BOTTOM_END"}
|
||||||
|
.menuCorner=${"END"}
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="trigger"
|
||||||
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
|
||||||
|
<ha-list-item graphic="icon" .disabled=${!this._uiAvailable}>
|
||||||
|
${this.hass.localize("ui.panel.lovelace.editor.edit_card.edit_ui")}
|
||||||
|
${!this._yamlMode
|
||||||
|
? html`
|
||||||
|
<ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiCheck}
|
||||||
|
></ha-svg-icon>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-list-item>
|
||||||
|
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.edit_card.edit_yaml"
|
||||||
|
)}
|
||||||
|
${this._yamlMode
|
||||||
|
? html`
|
||||||
|
<ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiCheck}
|
||||||
|
></ha-svg-icon>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-list-item>
|
||||||
|
</ha-button-menu>
|
||||||
|
</div>
|
||||||
|
${this._yamlMode
|
||||||
|
? html`
|
||||||
|
<ha-yaml-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.defaultValue=${this.config.layout_options}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-yaml-editor>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-grid-size-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this._gridSizeValue(
|
||||||
|
this.config.layout_options,
|
||||||
|
this._defaultLayoutOptions
|
||||||
|
)}
|
||||||
|
.isDefault=${this._isDefault(this.config.layout_options)}
|
||||||
|
@value-changed=${this._gridSizeChanged}
|
||||||
|
></ha-grid-size-picker>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues<this>): void {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
try {
|
||||||
|
this._cardElement = document.createElement("hui-card");
|
||||||
|
this._cardElement.hass = this.hass;
|
||||||
|
this._cardElement.preview = true;
|
||||||
|
this._cardElement.config = this.config;
|
||||||
|
this._cardElement.addEventListener("card-updated", (ev: Event) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._defaultLayoutOptions =
|
||||||
|
this._cardElement?.getElementLayoutOptions();
|
||||||
|
});
|
||||||
|
this._defaultLayoutOptions = this._cardElement.getElementLayoutOptions();
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues<this>): void {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (this._cardElement) {
|
||||||
|
if (changedProps.has("hass")) {
|
||||||
|
this._cardElement.hass = this.hass;
|
||||||
|
}
|
||||||
|
if (changedProps.has("config")) {
|
||||||
|
this._cardElement.config = this.config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||||
|
switch (ev.detail.index) {
|
||||||
|
case 0:
|
||||||
|
this._yamlMode = false;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this._yamlMode = true;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this._reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _reset() {
|
||||||
|
const newConfig = { ...this.config };
|
||||||
|
delete newConfig.layout_options;
|
||||||
|
this._yamlEditor?.setValue({});
|
||||||
|
fireEvent(this, "value-changed", { value: newConfig });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _gridSizeChanged(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const value = ev.detail.value;
|
||||||
|
|
||||||
|
const newConfig: LovelaceCardConfig = {
|
||||||
|
...this.config,
|
||||||
|
layout_options: {
|
||||||
|
...this.config.layout_options,
|
||||||
|
grid_columns: value.columns,
|
||||||
|
grid_rows: value.rows,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newConfig.layout_options!.grid_columns === undefined) {
|
||||||
|
delete newConfig.layout_options!.grid_columns;
|
||||||
|
}
|
||||||
|
if (newConfig.layout_options!.grid_rows === undefined) {
|
||||||
|
delete newConfig.layout_options!.grid_rows;
|
||||||
|
}
|
||||||
|
if (Object.keys(newConfig.layout_options!).length === 0) {
|
||||||
|
delete newConfig.layout_options;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", { value: newConfig });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const options = ev.detail.value as LovelaceLayoutOptions;
|
||||||
|
const newConfig: LovelaceCardConfig = {
|
||||||
|
...this.config,
|
||||||
|
layout_options: options,
|
||||||
|
};
|
||||||
|
fireEvent(this, "value-changed", { value: newConfig });
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.header .intro {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.header ha-button-menu {
|
||||||
|
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||||
|
margin-top: -8px;
|
||||||
|
}
|
||||||
|
.selected_menu_item {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
ha-grid-size-editor {
|
||||||
|
display: block;
|
||||||
|
max-width: 250px;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
ha-yaml-editor {
|
||||||
|
display: block;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-card-layout-editor": HuiCardLayoutEditor;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { LitElement, html } from "lit";
|
import { LitElement, html, css } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
@ -16,11 +16,11 @@ export class HuiCardVisibilityEditor extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
const conditions = this.config.visibility ?? [];
|
const conditions = this.config.visibility ?? [];
|
||||||
return html`
|
return html`
|
||||||
<ha-alert alert-type="info">
|
<p class="intro">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
`ui.panel.lovelace.editor.edit_card.visibility.explanation`
|
`ui.panel.lovelace.editor.edit_card.visibility.explanation`
|
||||||
)}
|
)}
|
||||||
</ha-alert>
|
</p>
|
||||||
<ha-card-conditions-editor
|
<ha-card-conditions-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.conditions=${conditions}
|
.conditions=${conditions}
|
||||||
@ -42,6 +42,14 @@ export class HuiCardVisibilityEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
fireEvent(this, "value-changed", { value: newConfig });
|
fireEvent(this, "value-changed", { value: newConfig });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.intro {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -236,6 +236,7 @@ export class HuiDialogEditCard
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="element-editor">
|
<div class="element-editor">
|
||||||
<hui-card-element-editor
|
<hui-card-element-editor
|
||||||
|
.showLayoutTab=${this._shouldShowLayoutTab()}
|
||||||
.showVisibilityTab=${this._cardConfig?.type !== "conditional"}
|
.showVisibilityTab=${this._cardConfig?.type !== "conditional"}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.lovelace=${this._params.lovelaceConfig}
|
.lovelace=${this._params.lovelaceConfig}
|
||||||
@ -352,6 +353,18 @@ export class HuiDialogEditCard
|
|||||||
return this._params!.path.length === 2;
|
return this._params!.path.length === 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _shouldShowLayoutTab(): boolean {
|
||||||
|
/**
|
||||||
|
* Only show layout tab for cards in a grid section
|
||||||
|
* In the future, every section and view should be able to bring their own editor for layout.
|
||||||
|
* For now, we limit it to grid sections as it's the only section type
|
||||||
|
* */
|
||||||
|
return (
|
||||||
|
this._isInSection &&
|
||||||
|
(!this._containerConfig.type || this._containerConfig.type === "grid")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private _cardConfigInSection = memoizeOne(
|
private _cardConfigInSection = memoizeOne(
|
||||||
(cardConfig?: LovelaceCardConfig) => {
|
(cardConfig?: LovelaceCardConfig) => {
|
||||||
const { cards, title, ...containerConfig } = this
|
const { cards, title, ...containerConfig } = this
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { preventDefault } from "@fullcalendar/core/internal";
|
|
||||||
import { ActionDetail } from "@material/mwc-list";
|
import { ActionDetail } from "@material/mwc-list";
|
||||||
import { mdiCheck, mdiDelete, mdiDotsVertical, mdiFlask } from "@mdi/js";
|
import { mdiCheck, mdiDelete, mdiDotsVertical, mdiFlask } from "@mdi/js";
|
||||||
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||||
@ -6,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
|
@ -14,7 +14,7 @@ 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 } from "../types";
|
import type { Lovelace, LovelaceLayoutOptions } from "../types";
|
||||||
|
|
||||||
const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||||
delay: 100,
|
delay: 100,
|
||||||
@ -23,6 +23,11 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
|||||||
invertedSwapThreshold: 0.7,
|
invertedSwapThreshold: 0.7,
|
||||||
} as HaSortableOptions;
|
} as HaSortableOptions;
|
||||||
|
|
||||||
|
export const DEFAULT_GRID_OPTIONS: LovelaceLayoutOptions = {
|
||||||
|
grid_columns: 4,
|
||||||
|
grid_rows: 1,
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@ -95,11 +100,15 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
|||||||
const card = this.cards![idx];
|
const card = this.cards![idx];
|
||||||
const layoutOptions = card.getLayoutOptions();
|
const layoutOptions = card.getLayoutOptions();
|
||||||
|
|
||||||
|
const columnSize =
|
||||||
|
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": layoutOptions.grid_columns,
|
"--column-size": columnSize,
|
||||||
"--row-size": layoutOptions.grid_rows,
|
"--row-size": rowSize,
|
||||||
})}
|
})}
|
||||||
class="card ${classMap({
|
class="card ${classMap({
|
||||||
"fit-rows": typeof layoutOptions?.grid_rows === "number",
|
"fit-rows": typeof layoutOptions?.grid_rows === "number",
|
||||||
@ -216,8 +225,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, 1);
|
grid-row: span var(--row-size);
|
||||||
grid-column: span var(--column-size, 4);
|
grid-column: span var(--column-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.fit-rows {
|
.card.fit-rows {
|
||||||
|
@ -122,6 +122,7 @@ export class HuiSection extends ReactiveElement {
|
|||||||
this._layoutElement.lovelace = this.lovelace;
|
this._layoutElement.lovelace = this.lovelace;
|
||||||
}
|
}
|
||||||
if (changedProperties.has("preview")) {
|
if (changedProperties.has("preview")) {
|
||||||
|
this._layoutElement.preview = this.preview;
|
||||||
this._cards.forEach((element) => {
|
this._cards.forEach((element) => {
|
||||||
element.preview = this.preview;
|
element.preview = this.preview;
|
||||||
});
|
});
|
||||||
@ -129,7 +130,7 @@ export class HuiSection extends ReactiveElement {
|
|||||||
if (changedProperties.has("_cards")) {
|
if (changedProperties.has("_cards")) {
|
||||||
this._layoutElement.cards = this._cards;
|
this._layoutElement.cards = this._cards;
|
||||||
}
|
}
|
||||||
if (changedProperties.has("hass") || changedProperties.has("lovelace")) {
|
if (changedProperties.has("hass") || changedProperties.has("preview")) {
|
||||||
this._updateElement();
|
this._updateElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,6 +210,7 @@ export class HUIView extends ReactiveElement {
|
|||||||
try {
|
try {
|
||||||
element.hass = this.hass;
|
element.hass = this.hass;
|
||||||
element.lovelace = this.lovelace;
|
element.lovelace = this.lovelace;
|
||||||
|
element.preview = this.lovelace.editMode;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this._rebuildSection(element, createErrorSectionConfig(e.message));
|
this._rebuildSection(element, createErrorSectionConfig(e.message));
|
||||||
}
|
}
|
||||||
|
@ -741,6 +741,11 @@
|
|||||||
"last_year": "Last year"
|
"last_year": "Last year"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"grid-size-picker": {
|
||||||
|
"reset_default": "Reset to default size",
|
||||||
|
"columns": "Number of columns",
|
||||||
|
"rows": "Number of rows"
|
||||||
|
},
|
||||||
"relative_time": {
|
"relative_time": {
|
||||||
"never": "Never"
|
"never": "Never"
|
||||||
},
|
},
|
||||||
@ -5507,10 +5512,14 @@
|
|||||||
"increase_position": "Increase card position",
|
"increase_position": "Increase card position",
|
||||||
"options": "More options",
|
"options": "More options",
|
||||||
"search_cards": "Search cards",
|
"search_cards": "Search cards",
|
||||||
"tab-config": "Config",
|
"tab_config": "Config",
|
||||||
"tab-visibility": "Visibility",
|
"tab_visibility": "Visibility",
|
||||||
|
"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": {
|
||||||
|
"explanation": "Configure how the card will appear on the dashboard. This settings will override the default size and position of the card."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"move_card": {
|
"move_card": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user