ha-frontend/src/panels/lovelace/editor/conditions/types/ha-card-condition-screen.ts

204 lines
5.9 KiB
TypeScript

import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getAllCombinations } from "../../../../../common/array/combinations";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import { HaFormSchema } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import { LovelaceScreenCondition } from "../../../common/conditions/types";
const BREAKPOINT_VALUES = [0, 768, 1024, 1280, Infinity];
const BREAKPOINTS = ["mobile", "tablet", "desktop", "wide"] as const;
type BreakpointSize = [number, number];
type Breakpoint = (typeof BREAKPOINTS)[number];
function mergeConsecutiveRanges(arr: [number, number][]): [number, number][] {
if (arr.length === 0) {
return [];
}
[...arr].sort((a, b) => a[0] - b[0]);
const mergedRanges = [arr[0]];
for (let i = 1; i < arr.length; i++) {
const currentRange = arr[i];
const previousRange = mergedRanges[mergedRanges.length - 1];
if (currentRange[0] <= previousRange[1] + 1) {
previousRange[1] = currentRange[1];
} else {
mergedRanges.push(currentRange);
}
}
return mergedRanges;
}
function buildMediaQuery(size: BreakpointSize) {
const [min, max] = size;
const query: string[] = [];
if (min != null) {
query.push(`(min-width: ${min}px)`);
}
if (max != null && max !== Infinity) {
query.push(`(max-width: ${max - 1}px)`);
}
return query.join(" and ");
}
function computeBreakpointsSize(breakpoints: Breakpoint[]) {
const sizes = breakpoints.map<BreakpointSize>((breakpoint) => {
const index = BREAKPOINTS.indexOf(breakpoint);
return [BREAKPOINT_VALUES[index], BREAKPOINT_VALUES[index + 1] || Infinity];
});
const mergedSizes = mergeConsecutiveRanges(sizes);
const queries = mergedSizes
.map((size) => buildMediaQuery(size))
.filter((size) => size);
return queries.join(", ");
}
function computeBreakpointsKey(breakpoints) {
return [...breakpoints].sort().join("_");
}
// Compute all possible media queries from each breakpoints combination (2 ^ breakpoints = 16)
const queries = getAllCombinations(BREAKPOINTS as unknown as Breakpoint[])
.filter((arr) => arr.length !== 0)
.map(
(breakpoints) =>
[breakpoints, computeBreakpointsSize(breakpoints)] as [
Breakpoint[],
string,
]
);
// Store them in maps to avoid recomputing them
const mediaQueryMap = new Map(
queries.map(([b, m]) => [computeBreakpointsKey(b), m])
);
const mediaQueryReverseMap = new Map(queries.map(([b, m]) => [m, b]));
type ScreenConditionData = {
breakpoints: Breakpoint[];
};
@customElement("ha-card-condition-screen")
export class HaCardConditionScreen extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: LovelaceScreenCondition;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): LovelaceScreenCondition {
return { condition: "screen", media_query: "" };
}
protected static validateUIConfig(
condition: LovelaceScreenCondition,
hass: HomeAssistant
) {
const valid =
!condition.media_query || mediaQueryReverseMap.has(condition.media_query);
if (!valid) {
throw new Error(
hass.localize("ui.errors.config.media_query_not_supported")
);
}
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "breakpoints",
selector: {
select: {
mode: "list",
options: BREAKPOINTS.map((b) => {
const value = BREAKPOINT_VALUES[BREAKPOINTS.indexOf(b)];
return {
value: b,
label: `${localize(
`ui.panel.lovelace.editor.condition-editor.condition.screen.breakpoints_list.${b}`
)}${
value
? ` (${localize(
`ui.panel.lovelace.editor.condition-editor.condition.screen.min`,
{ size: value }
)})`
: ""
}`,
};
}),
multiple: true,
},
},
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
const breakpoints = this.condition.media_query
? mediaQueryReverseMap.get(this.condition.media_query)
: undefined;
const data: ScreenConditionData = {
breakpoints: breakpoints ?? [],
};
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${this._schema(this.hass.localize)}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const data = ev.detail.value as ScreenConditionData;
const { breakpoints } = data;
const condition: LovelaceScreenCondition = {
condition: "screen",
media_query: mediaQueryMap.get(computeBreakpointsKey(breakpoints)) ?? "",
};
fireEvent(this, "value-changed", { value: condition });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "breakpoints":
return this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.condition.screen.${schema.name}`
);
default:
return "";
}
};
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-screen": HaCardConditionScreen;
}
}