mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-04 21:52:59 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2599f97aae | |||
| 1b6e150def | |||
| 939b7012e2 | |||
| 488d3d9b37 | |||
| 08dedc415f | |||
| 400bf78ce0 | |||
| 9b2093125e | |||
| c0d963747a | |||
| 1bcd43fb75 | |||
| ce5925388c | |||
| 088f72fcc4 | |||
| b93f09ffb2 | |||
| fb499093e5 | |||
| c97716d4ad | |||
| b36361ca34 | |||
| 007a7916c0 | |||
| 637f6955a8 | |||
| b574aaa084 | |||
| c1dca9b377 | |||
| c9963b7f92 | |||
| 42954152dc | |||
| f5a28977c2 | |||
| 9b1f407ff4 | |||
| 277822e51c | |||
| 4a2bca6d76 | |||
| 8e47b5369e | |||
| 759ab155c7 | |||
| 8de939a696 | |||
| 5c3d74502b | |||
| 0bf67b603c | |||
| 01915f2e5a | |||
| cee1c91ed8 |
@@ -7,6 +7,8 @@ description: Home Assistant frontend component patterns. Use when implementing o
|
||||
|
||||
Use this skill when creating or reviewing Home Assistant UI components and common interaction patterns.
|
||||
|
||||
Cross-load `ha-frontend-events` when component work includes event listener typing, custom event dispatch, or event-map declarations.
|
||||
|
||||
## Dialogs
|
||||
|
||||
Open dialogs through the fire-event pattern:
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
name: ha-frontend-events
|
||||
description: Home Assistant frontend event patterns. Use when typing event handlers, using HASSDomEvent types, dispatching with fireEvent, or declaring HASSDomEvents and event maps.
|
||||
---
|
||||
|
||||
# HA Frontend Events
|
||||
|
||||
Use this skill when implementing or reviewing event listeners and custom event contracts. Cross-load `ha-frontend-components` when the work also involves dialogs, forms, alerts, shortcuts, tooltips, panels, Lovelace cards, or buttons.
|
||||
|
||||
## Event Handling
|
||||
|
||||
Use the event types from `src/common/dom/fire_event.ts` instead of plain `Event`, generic `CustomEvent`, or element casts when they express the handler contract:
|
||||
|
||||
- Use `HASSDomCurrentTargetEvent<T>` to read the element on which the listener was registered through `ev.currentTarget`.
|
||||
- Use `HASSDomTargetEvent<T>` only to read the element that originated the event through `ev.target`.
|
||||
- Use `HASSDomEvent<T>` to read a custom event payload through `ev.detail`.
|
||||
- Use `ValueChangedEvent<T>` from `src/types.ts` for the standard `value-changed` event.
|
||||
- Prefer an event type exported by the component being listened to, such as `HaSelectSelectEvent<T, Clearable>` or `HaDropdownSelectEvent<TValue, TData>`, over reconstructing its detail type.
|
||||
- Use `ActionHandlerEvent` from `src/data/lovelace/action_handler.ts` for Lovelace tap, hold, and double-tap handlers.
|
||||
|
||||
Import event and element types with `import type`:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import type { HaCheckbox } from "../components/ha-checkbox";
|
||||
import type { HaEntityPicker } from "../components/entity/ha-entity-picker";
|
||||
import type { HaRadioGroup } from "../components/radio/ha-radio-group";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
```
|
||||
|
||||
Type the handler so the selected property can be read directly. Do not cast `ev.currentTarget` or assign it to a single-use variable:
|
||||
|
||||
```ts
|
||||
private _scopeChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>): void {
|
||||
this._scope = ev.currentTarget.value;
|
||||
}
|
||||
|
||||
private _checkedChanged(ev: HASSDomTargetEvent<HaCheckbox>): void {
|
||||
this._checked = ev.target.checked;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>): void {
|
||||
this._value = ev.detail.value;
|
||||
}
|
||||
|
||||
private _itemSelected(ev: HASSDomEvent<{ id: string }>): void {
|
||||
this._selectedId = ev.detail.id;
|
||||
}
|
||||
```
|
||||
|
||||
Use intersections when a handler needs more than one facet of an event. Keep the native event type when the handler reads native fields such as `key`, modifier keys, `dataTransfer`, or focus relationships:
|
||||
|
||||
```ts
|
||||
private _entityChanged(
|
||||
ev: ValueChangedEvent<string> & HASSDomCurrentTargetEvent<HaEntityPicker>
|
||||
): void {
|
||||
ev.currentTarget.value = ev.detail.value;
|
||||
}
|
||||
|
||||
private _keyDown(
|
||||
ev: KeyboardEvent & HASSDomCurrentTargetEvent<HTMLInputElement>
|
||||
): void {
|
||||
if (ev.key === "Enter") {
|
||||
this._submit(ev.currentTarget.value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Dispatch Home Assistant component events with `fireEvent()` instead of constructing `Event` or `CustomEvent` directly. Register the event name and detail type by augmenting `HASSDomEvents`; use `undefined` when an event has no detail. `fireEvent()` constrains event names and supplied detail, and events bubble and cross shadow boundaries by default:
|
||||
|
||||
```ts
|
||||
fireEvent(this, "item-selected", { id: item.id });
|
||||
fireEvent(this, "refresh-requested");
|
||||
```
|
||||
|
||||
When an event is already registered, derive handler and listener types from its registration rather than repeating the payload shape:
|
||||
|
||||
```ts
|
||||
private _itemSelected(
|
||||
ev: HASSDomEvent<HASSDomEvents["item-selected"]>
|
||||
): void {
|
||||
this._selectedId = ev.detail.id;
|
||||
}
|
||||
```
|
||||
|
||||
`HASSDomEvents` types `fireEvent()` calls. Augment `HTMLElementEventMap` for typed listeners on HTML elements, or `GlobalEventHandlersEventMap` when the event is handled on global event targets.
|
||||
|
||||
In component files, prefer placing global event declarations after the class at the bottom of the file. Preserve the existing placement when editing established files; foundational type, helper, and mixin files commonly keep declarations near the top before their consumers.
|
||||
|
||||
```ts
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"item-selected": { id: string };
|
||||
"refresh-requested": undefined;
|
||||
}
|
||||
|
||||
interface HTMLElementEventMap {
|
||||
"item-selected": HASSDomEvent<HASSDomEvents["item-selected"]>;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: ha-frontend-lit
|
||||
description: Home Assistant frontend Lit conventions. Use when working with reactive properties, internal state, DOM queries, lifecycle methods, or render-derived state.
|
||||
---
|
||||
|
||||
# HA Frontend Lit
|
||||
|
||||
Use this skill when implementing or reviewing Lit component state, DOM access, lifecycle methods, or rendering behavior. Cross-load `ha-frontend-types` for Home Assistant data contracts, assertions, and lifecycle parameter types.
|
||||
|
||||
## Reactive Fields
|
||||
|
||||
This project currently uses Lit's TypeScript experimental decorators with `useDefineForClassFields: false`. Match existing declarations and do not introduce standard-decorator `accessor` syntax unless the project changes decorator mode.
|
||||
|
||||
- Use `@property()` for public reactive API and `@state()` for private reactive state.
|
||||
- Prefer inferred types for initialized reactive fields when inference preserves the intended type; annotate when widening or an external contract requires it.
|
||||
|
||||
## DOM Queries
|
||||
|
||||
Prefer Lit's `@query()` or `@queryAll()` decorators for fixed selectors in the component's render root.
|
||||
|
||||
- Type the decorated field with the narrowest useful DOM or component interface.
|
||||
- Keep the field optional when it may be absent at the point of access, including conditional rendering or pre-render lifecycle access.
|
||||
- Use a definite assignment assertion only when every call site runs after the node is guaranteed to exist.
|
||||
- The optional second argument to `@query()`, as in `@query("#target", true)`, caches the first query result. Use it only when later renders cannot replace the queried node.
|
||||
- Use a direct query when the selector is dynamic or the target is outside the component's render root. Before querying a child, consider whether the required value belongs in parent state or data flow.
|
||||
|
||||
## Render-Derived State
|
||||
|
||||
- Prefer render-local values for inexpensive structures used only by that render.
|
||||
- Assign a render-local value once when repeated evaluation is non-trivial or a local name improves clarity.
|
||||
- Keep purely presentational derivations in `render()`. Use stored state or `willUpdate()` when the value must participate in lifecycle work, reflection, CSS, or non-render consumers.
|
||||
- Use `memoizeOne` for pure, argument-derived transforms when stable input identity avoids meaningful repeated work. Keep inputs explicit and limited, and do not add caching without a credible benefit over computing the value directly.
|
||||
|
||||
## References
|
||||
|
||||
- [Reactive properties](https://lit.dev/docs/components/properties/)
|
||||
- [Decorators](https://lit.dev/docs/components/decorators/)
|
||||
- [Shadow DOM queries](https://lit.dev/docs/components/shadow-dom/#query)
|
||||
- [Reactive update cycle](https://lit.dev/docs/components/lifecycle/)
|
||||
@@ -31,6 +31,33 @@ When creating a pull request, use `.github/PULL_REQUEST_TEMPLATE.md` as the body
|
||||
|
||||
## Recurring Review Issues
|
||||
|
||||
Scope and public surface:
|
||||
|
||||
- Keep changes independently reviewable and limited to the requested area.
|
||||
- Prefer existing Home Assistant helpers, Lit primitives, and component seams over parallel implementations.
|
||||
- Challenge new public properties and optional feature surface when transient options or existing seams meet the requirement with less lifecycle and consistency cost.
|
||||
|
||||
Stateful and asynchronous UI:
|
||||
|
||||
- Review transitions in both directions, not only individual rendered states.
|
||||
- When controls reappear, restore valid defaults instead of retaining state that was only valid while they were hidden.
|
||||
- Establish immutable dirty-state baselines before asynchronous work, guard against stale responses, and preserve unsaved state in mounted editors.
|
||||
- Determine an action's current meaning before applying dirty-state checks, especially when an action can change between Save and Close.
|
||||
|
||||
Readiness and invalidation:
|
||||
|
||||
- Treat readiness as the first displayable terminal result, including stable empty and error states.
|
||||
- Register child readiness before resolving the parent, do not treat fallback work as terminal, and replay readiness correctly for cached or reused panels.
|
||||
- Ensure every value read by memoized output participates in its invalidation.
|
||||
|
||||
Repository-owned contracts:
|
||||
|
||||
- Consult the public [frontend developer documentation](https://developers.home-assistant.io/docs/frontend/) for documented architecture, data flow, design, and development workflows.
|
||||
- For new leaf components, load `ha-frontend-contexts` and verify they consume narrow contexts instead of introducing a broad `hass` property; containers and external APIs may still require `hass`.
|
||||
- Verify backend assumptions against the owning Core, Supervisor, or WebSocket implementation, and component assumptions against the exported component contract.
|
||||
- Prefer canonical repository helpers and test setup over duplicate local implementations.
|
||||
- Promote AI-review concerns into durable guidance only when supported by code evidence, reproduced behavior, an accepted corrective commit, or human-maintainer validation.
|
||||
|
||||
User experience and accessibility:
|
||||
|
||||
- Forms need proper labels, helper text, and validation feedback.
|
||||
@@ -70,4 +97,4 @@ Configuration and props:
|
||||
- Identify behavioral regressions, bugs, accessibility issues, and missing tests first.
|
||||
- Keep style-only comments secondary unless they affect maintainability or user experience.
|
||||
- Prefer small, direct fixes over large refactors during review follow-up.
|
||||
- Cross-load `ha-frontend-contexts`, `ha-frontend-components`, `ha-frontend-styling`, `ha-frontend-testing`, or `ha-frontend-user-facing-text` when a finding falls in that area.
|
||||
- Load the matching `ha-frontend-*` skill when a finding falls within its area.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: ha-frontend-types
|
||||
description: Home Assistant frontend TypeScript conventions. Use when defining or reviewing backend data contracts, optional schemas, shared types, assertions, or Lit lifecycle types.
|
||||
---
|
||||
|
||||
# HA Frontend Types
|
||||
|
||||
Use this skill for Home Assistant-specific TypeScript contracts and type choices.
|
||||
|
||||
## Home Assistant Data Contracts
|
||||
|
||||
Verify data contracts against the source that owns them, such as Home Assistant Core, Supervisor, a WebSocket handler, or an exported component type. Do not shape a type around assumptions made by its current frontend consumers.
|
||||
|
||||
- Match required, optional, nullable, and defaulted fields to the producer and runtime contract. Preserve optional configuration fields when omission is supported and has defined behavior.
|
||||
- Use distinct request and response types when their wire shapes differ.
|
||||
- When changing a shared contract, check affected consumers, tests, fixtures, and mocks.
|
||||
|
||||
## Reuse Home Assistant Contracts
|
||||
|
||||
- Reuse the canonical Home Assistant type when one exists. Define shared contract types in the data or API module that owns them.
|
||||
- Reuse types exported by components and helpers rather than reconstructing their payloads. For event types, follow `ha-frontend-events`.
|
||||
- When one domain contract is used across modules, define and export it from the module that owns that contract.
|
||||
|
||||
Prefer an existing owning contract. Introduce a frontend-specific type when the frontend shape or boundary genuinely differs.
|
||||
|
||||
## Assertions
|
||||
|
||||
Prefer accurate types and runtime narrowing. Use assertions or TypeScript suppressions at boundaries where the runtime invariant is understood but cannot be expressed cleanly; keep them narrow and explain non-obvious invariants.
|
||||
|
||||
## Lit Lifecycle Types
|
||||
|
||||
For Lit lifecycle methods that receive changed properties, use `PropertyValues<this>` when the method only needs public reactive properties:
|
||||
|
||||
```ts
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Use unparameterized `PropertyValues` when the method inspects private or protected reactive properties, which are not keys of `this`. Do not add assertions solely to retain `PropertyValues<this>`.
|
||||
|
||||
## Enforced Baseline
|
||||
|
||||
Use `import type` for type-only imports. This is enforced by the repository ESLint configuration.
|
||||
@@ -40,6 +40,9 @@ Detailed guidance lives in project skills under `.agents/skills/`. Load the matc
|
||||
|
||||
- `ha-frontend-contexts`: Lit contexts, `hass` migration, and rerender-sensitive state access.
|
||||
- `ha-frontend-components`: dialogs, forms, alerts, shortcuts, tooltips, panels, and Lovelace cards.
|
||||
- `ha-frontend-events`: event handler typing, custom event dispatch, and event-map declarations.
|
||||
- `ha-frontend-types`: backend data contracts, optional schemas, shared types, assertions, and lifecycle types.
|
||||
- `ha-frontend-lit`: reactive fields, DOM queries, lifecycle behavior, and render-derived state.
|
||||
- `ha-frontend-styling`: theme variables, spacing tokens, responsive layout, RTL, and view transitions.
|
||||
- `ha-frontend-testing`: lint, typecheck, Vitest, Playwright e2e dev servers, and benchmarks.
|
||||
- `ha-frontend-user-facing-text`: localization, terminology, sentence case, and Home Assistant text style.
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { constants } from "node:zlib";
|
||||
import gulp from "gulp";
|
||||
import brotli from "gulp-brotli";
|
||||
import zopfli from "gulp-zopfli-green";
|
||||
import paths from "../paths.cjs";
|
||||
import zopfli from "../zopfli.mjs";
|
||||
|
||||
const filesGlob = "*.{js,json,css,svg,xml}";
|
||||
const brotliOptions = {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Worker side of the zopfli pool. @gfx/zopfli is a synchronous WASM build, so
|
||||
// compressing on the main thread blocks the event loop; one instance per worker
|
||||
// is what makes the work parallel.
|
||||
|
||||
import { parentPort } from "node:worker_threads";
|
||||
import zopfli from "@gfx/zopfli";
|
||||
|
||||
parentPort.on("message", ({ contents, options }) => {
|
||||
zopfli.gzip(contents, options, (error, result) => {
|
||||
if (error) {
|
||||
parentPort.postMessage({ error: error.message ?? String(error) });
|
||||
return;
|
||||
}
|
||||
// `result` is a fresh Uint8Array copied out of the WASM heap, so it owns
|
||||
// its ArrayBuffer and can be transferred instead of cloned.
|
||||
parentPort.postMessage({ result }, [result.buffer]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
// Gulp transform that gzips files with zopfli across a pool of worker threads.
|
||||
//
|
||||
// Drop-in replacement for gulp-zopfli-green. That plugin compresses on the main
|
||||
// thread, and @gfx/zopfli is synchronous WASM, so it pins a single core and
|
||||
// blocks the event loop for the whole compression step. Running one WASM
|
||||
// instance per worker parallelises it; the output bytes are unchanged.
|
||||
|
||||
import { availableParallelism } from "node:os";
|
||||
import { Transform } from "node:stream";
|
||||
import { buffer as readStream } from "node:stream/consumers";
|
||||
import { Worker } from "node:worker_threads";
|
||||
|
||||
const WORKER_URL = new URL("./zopfli-worker.mjs", import.meta.url);
|
||||
const EXTENSION = ".gz";
|
||||
|
||||
// Left empty on purpose: @gfx/zopfli then applies its own defaults, which is
|
||||
// what gulp-zopfli-green did, so compressed output stays byte-identical.
|
||||
const ZOPFLI_OPTIONS = {};
|
||||
|
||||
const poolSize = () => {
|
||||
const configured = Number(process.env.ZOPFLI_WORKERS);
|
||||
return Number.isInteger(configured) && configured > 0
|
||||
? configured
|
||||
: availableParallelism();
|
||||
};
|
||||
|
||||
// One job per worker at a time, so the worker itself is the job slot.
|
||||
const createPool = (size) => {
|
||||
const live = new Set();
|
||||
const idle = [];
|
||||
const waiting = [];
|
||||
const inFlight = new Map();
|
||||
|
||||
// A dead worker must leave the pool, or it gets handed a job that never
|
||||
// completes and the build hangs instead of failing.
|
||||
const retire = (worker, error) => {
|
||||
if (!live.delete(worker)) {
|
||||
return;
|
||||
}
|
||||
const index = idle.indexOf(worker);
|
||||
if (index !== -1) {
|
||||
idle.splice(index, 1);
|
||||
}
|
||||
const job = inFlight.get(worker);
|
||||
inFlight.delete(worker);
|
||||
job?.reject(error);
|
||||
waiting.shift()?.();
|
||||
};
|
||||
|
||||
const spawn = () => {
|
||||
const worker = new Worker(WORKER_URL);
|
||||
live.add(worker);
|
||||
// Idle workers must not hold the process open. Each job refs its worker for
|
||||
// as long as it runs, so a pending compression always keeps the event loop
|
||||
// alive.
|
||||
worker.unref();
|
||||
|
||||
worker.on("message", ({ error, result }) => {
|
||||
const job = inFlight.get(worker);
|
||||
if (!job) {
|
||||
return;
|
||||
}
|
||||
inFlight.delete(worker);
|
||||
worker.unref();
|
||||
idle.push(worker);
|
||||
if (error) {
|
||||
job.reject(new Error(error));
|
||||
} else {
|
||||
job.resolve(
|
||||
Buffer.from(result.buffer, result.byteOffset, result.byteLength)
|
||||
);
|
||||
}
|
||||
waiting.shift()?.();
|
||||
});
|
||||
|
||||
worker.on("error", (error) => retire(worker, error));
|
||||
worker.on("exit", () =>
|
||||
retire(worker, new Error("zopfli worker exited unexpectedly"))
|
||||
);
|
||||
|
||||
return worker;
|
||||
};
|
||||
|
||||
return (contents) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const start = () => {
|
||||
const worker = idle.pop() ?? (live.size < size ? spawn() : undefined);
|
||||
if (!worker) {
|
||||
waiting.push(start);
|
||||
return;
|
||||
}
|
||||
inFlight.set(worker, { resolve, reject });
|
||||
worker.ref();
|
||||
// `contents` is cloned rather than transferred: buffers read by vinyl
|
||||
// can share a pooled ArrayBuffer with unrelated buffers, and
|
||||
// transferring would detach those too.
|
||||
worker.postMessage({ contents, options: ZOPFLI_OPTIONS });
|
||||
};
|
||||
start();
|
||||
});
|
||||
};
|
||||
|
||||
// Shared by every transform this module hands out, so the worker count is a
|
||||
// property of the process rather than of how many streams happen to run.
|
||||
let pool;
|
||||
|
||||
const sharedPool = () => {
|
||||
if (!pool) {
|
||||
const size = poolSize();
|
||||
pool = { size, compress: createPool(size) };
|
||||
}
|
||||
return pool;
|
||||
};
|
||||
|
||||
// through2 and node's Transform both wait for the previous callback before
|
||||
// handling the next file, which would serialise the pool down to one worker.
|
||||
// This keeps `limit` files in flight and applies backpressure beyond that.
|
||||
class ParallelTransform extends Transform {
|
||||
#limit;
|
||||
|
||||
#handle;
|
||||
|
||||
#inFlight = 0;
|
||||
|
||||
#resume;
|
||||
|
||||
#finish;
|
||||
|
||||
constructor(limit, handle) {
|
||||
super({ objectMode: true, highWaterMark: limit });
|
||||
this.#limit = limit;
|
||||
this.#handle = handle;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
|
||||
_transform(file, _encoding, callback) {
|
||||
this.#inFlight += 1;
|
||||
this.#handle(file)
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
this.push(result);
|
||||
}
|
||||
})
|
||||
.catch((error) => this.destroy(error))
|
||||
.finally(() => {
|
||||
this.#inFlight -= 1;
|
||||
const resume = this.#resume;
|
||||
this.#resume = undefined;
|
||||
resume?.();
|
||||
if (this.#inFlight === 0) {
|
||||
const finish = this.#finish;
|
||||
this.#finish = undefined;
|
||||
finish?.();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.#inFlight < this.#limit) {
|
||||
callback();
|
||||
} else {
|
||||
this.#resume = callback;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
|
||||
_flush(callback) {
|
||||
if (this.#inFlight === 0) {
|
||||
callback();
|
||||
} else {
|
||||
this.#finish = callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.threshold] Skip files smaller than this many bytes.
|
||||
*/
|
||||
export default ({ threshold = 0 } = {}) => {
|
||||
const { size, compress } = sharedPool();
|
||||
|
||||
return new ParallelTransform(size, async (file) => {
|
||||
if (file.isNull()) {
|
||||
return file;
|
||||
}
|
||||
if (file.isStream()) {
|
||||
file.contents = await readStream(file.contents);
|
||||
}
|
||||
if (threshold && file.contents.length < threshold) {
|
||||
// Passed through unrenamed and uncompressed, as gulp-zopfli-green did.
|
||||
return file;
|
||||
}
|
||||
file.contents = await compress(file.contents);
|
||||
file.path += EXTENSION;
|
||||
return file;
|
||||
});
|
||||
};
|
||||
+29
-1
@@ -111,7 +111,17 @@ export default tseslint.config(
|
||||
"no-bitwise": "error",
|
||||
"no-console": "error",
|
||||
"no-restricted-globals": [2, "event"],
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
"LabeledStatement",
|
||||
"WithStatement",
|
||||
{
|
||||
selector:
|
||||
"CallExpression[callee.property.name=/^(push|replace)State$/]",
|
||||
message:
|
||||
"Use navigate(), updateHistoryState() or replaceCurrentUrl() from common/navigate. History entries carry the app's own bookkeeping, which a raw pushState/replaceState drops.",
|
||||
},
|
||||
],
|
||||
"wc/no-self-class": "off",
|
||||
|
||||
// import-x rules
|
||||
@@ -222,6 +232,24 @@ export default tseslint.config(
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// These own history entries themselves: the navigation helpers, the dialog
|
||||
// stack, the boot paths that run before the app has any state to keep, and
|
||||
// the tests that fabricate entries to simulate a document load.
|
||||
files: [
|
||||
"src/common/navigate.ts",
|
||||
"src/dialogs/make-dialog-manager.ts",
|
||||
"src/state/url-sync-mixin.ts",
|
||||
"src/panels/config/automation/add-automation-element-dialog.ts",
|
||||
"src/entrypoints/core.ts",
|
||||
"src/onboarding/**/*.ts",
|
||||
"cast/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
],
|
||||
rules: {
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/util/recorder-worklet.js"],
|
||||
languageOptions: {
|
||||
|
||||
@@ -23,7 +23,7 @@ export class DemoHaProgressButton extends LitElement {
|
||||
<ha-progress-button @click=${this._clickedFail}>
|
||||
Fail
|
||||
</ha-progress-button>
|
||||
<ha-progress-button size="small" @click=${this._clickedSuccess}>
|
||||
<ha-progress-button size="s" @click=${this._clickedSuccess}>
|
||||
small
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
import { availableParallelism } from "node:os";
|
||||
import "./build-scripts/gulp/index.mjs";
|
||||
|
||||
process.env.UV_THREADPOOL_SIZE = availableParallelism();
|
||||
|
||||
+6
-6
@@ -103,12 +103,11 @@
|
||||
"echarts": "6.1.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.5.0",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "11.2.13",
|
||||
"js-yaml": "5.2.2",
|
||||
"js-yaml": "5.2.3",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
@@ -145,6 +144,7 @@
|
||||
"@babel/preset-env": "8.0.2",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@gfx/zopfli": "1.0.15",
|
||||
"@html-eslint/eslint-plugin": "0.64.0",
|
||||
"@lokalise/node-api": "16.3.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
@@ -160,11 +160,11 @@
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/culori": "4.0.1",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/leaflet": "1.9.21",
|
||||
"@types/leaflet": "1.9.22",
|
||||
"@types/leaflet-draw": "1.0.13",
|
||||
"@types/leaflet.markercluster": "1.5.6",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.7.2",
|
||||
"@types/luxon": "3.7.3",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
@@ -196,7 +196,7 @@
|
||||
"jsdom": "30.0.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lint-staged": "17.2.0",
|
||||
"lint-staged": "17.3.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
@@ -230,6 +230,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.18.0",
|
||||
"volta": {
|
||||
"node": "24.18.1"
|
||||
"node": "24.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,10 +8,10 @@
|
||||
":prConcurrentLimit10",
|
||||
":semanticCommitsDisabled",
|
||||
"group:monorepos",
|
||||
"group:recommended",
|
||||
"security:minimumReleaseAgeNpm"
|
||||
"group:recommended"
|
||||
],
|
||||
"enabledManagers": ["npm", "nvm", "custom.regex"],
|
||||
"minimumReleaseAge": "3 days",
|
||||
"postUpdateOptions": ["yarnDedupeHighest"],
|
||||
"lockFileMaintenance": {
|
||||
"description": ["Run after patch releases but before next beta"],
|
||||
|
||||
@@ -294,3 +294,26 @@ export const formatCallyDateRange = (
|
||||
|
||||
return `${startDate}/${endDate}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* August 2021, for calendar days coming out of cally.
|
||||
*
|
||||
* cally hands out calendar days as `Date` objects anchored to UTC midnight, and
|
||||
* formats its own headings in UTC too. Anything derived from a cally event has
|
||||
* to be formatted the same way: the resolved time zone shifts the day back for
|
||||
* negative UTC offsets, which becomes a wrong month — and in January a wrong
|
||||
* year — whenever the day is the 1st.
|
||||
*/
|
||||
export const formatCallyMonthYear = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData
|
||||
) => formatCallyMonthYearMem(locale.language).format(dateObj);
|
||||
|
||||
const formatCallyMonthYearMem = memoizeOne(
|
||||
(language: string) =>
|
||||
new Intl.DateTimeFormat(language, {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { ReactiveElement } from "lit";
|
||||
import { updateHistoryState } from "../navigate";
|
||||
import { throttle } from "../util/throttle";
|
||||
|
||||
const throttleReplaceState = throttle((value) => {
|
||||
history.replaceState({ scrollPosition: value }, "");
|
||||
updateHistoryState({ scrollPosition: value });
|
||||
}, 300);
|
||||
|
||||
export function restoreScroll(selector: string) {
|
||||
|
||||
+59
-39
@@ -1,6 +1,7 @@
|
||||
import { closeAllDialogs } from "../dialogs/make-dialog-manager";
|
||||
import { fireEvent } from "./dom/fire_event";
|
||||
import { mainWindow } from "./dom/get_main_window";
|
||||
import { currentPath } from "./url/current-path";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -17,6 +18,26 @@ export interface NavigateOptions {
|
||||
// max time to wait for dialogs to close before navigating
|
||||
const DIALOG_WAIT_TIMEOUT = 500;
|
||||
|
||||
/**
|
||||
* Merge into the current history entry's state, keeping what is already there.
|
||||
* Entries carry the app's own bookkeeping (`from`, `root`, dialog state), so
|
||||
* they must never be replaced wholesale.
|
||||
*/
|
||||
export const updateHistoryState = (patch: Record<string, unknown>) => {
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, ...patch },
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrite the URL of the current history entry without navigating and without
|
||||
* touching its state. For query parameter cleanup.
|
||||
*/
|
||||
export const replaceCurrentUrl = (url: string) => {
|
||||
mainWindow.history.replaceState(mainWindow.history.state, "", url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stash a destination URL in the current history entry's state. If the page
|
||||
* is refreshed while a dialog is open, urlSyncMixin will navigate to this URL
|
||||
@@ -24,10 +45,7 @@ const DIALOG_WAIT_TIMEOUT = 500;
|
||||
* The current URL is not changed.
|
||||
*/
|
||||
export const setRefreshUrl = (path: string) => {
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, refreshUrl: path },
|
||||
""
|
||||
);
|
||||
updateHistoryState({ refreshUrl: path });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -63,37 +81,32 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
}
|
||||
const replace = options?.replace || false;
|
||||
|
||||
if (__DEMO__) {
|
||||
if (!path.includes("#")) {
|
||||
// The demo routes with the hash instead of the pathname. Resolve the
|
||||
// path like the browser would do for pushState, and keep the query
|
||||
// parameters in the URL query instead of inside the hash.
|
||||
const url = new URL(
|
||||
path,
|
||||
`${mainWindow.location.origin}${mainWindow.location.hash.substring(1)}`
|
||||
);
|
||||
path = `${mainWindow.location.pathname}${url.search}#${url.pathname}`;
|
||||
}
|
||||
if (replace) {
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root
|
||||
? { root: true }
|
||||
: (options?.data ?? null),
|
||||
"",
|
||||
path
|
||||
);
|
||||
} else {
|
||||
mainWindow.history.pushState(options?.data ?? null, "", path);
|
||||
}
|
||||
} else if (replace) {
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root ? { root: true } : (options?.data ?? null),
|
||||
if (__DEMO__ && !path.includes("#")) {
|
||||
// The demo routes with the hash instead of the pathname. Resolve the
|
||||
// path like the browser would do for pushState, and keep the query
|
||||
// parameters in the URL query instead of inside the hash.
|
||||
const url = new URL(
|
||||
path,
|
||||
`${mainWindow.location.origin}${mainWindow.location.hash.substring(1)}`
|
||||
);
|
||||
path = `${mainWindow.location.pathname}${url.search}#${url.pathname}`;
|
||||
}
|
||||
|
||||
const { history } = mainWindow;
|
||||
|
||||
if (replace) {
|
||||
// A replaced entry keeps its predecessor, so it keeps `from`.
|
||||
const { root, from } = history.state ?? {};
|
||||
const state = root ? { root: true } : (options?.data ?? null);
|
||||
history.replaceState(
|
||||
from === undefined ? state : { ...state, from },
|
||||
"",
|
||||
path
|
||||
);
|
||||
} else {
|
||||
mainWindow.history.pushState(options?.data ?? null, "", path);
|
||||
history.pushState({ ...options?.data, from: currentPath() }, "", path);
|
||||
}
|
||||
|
||||
fireEvent(mainWindow, "location-changed", {
|
||||
replace,
|
||||
});
|
||||
@@ -101,8 +114,17 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate back in history, with fallback to a default path if no history exists.
|
||||
* This prevents a user from getting stuck when they navigate directly to a page with no history.
|
||||
* Whether the previous history entry is a page this app navigated away from.
|
||||
* `history.length` cannot answer this: a login redirect goes through
|
||||
* `location.assign`, which leaves /auth/authorize right behind the requested
|
||||
* page, and going back there would bounce the user out of the app.
|
||||
*/
|
||||
export const canGoBack = (): boolean =>
|
||||
mainWindow.history.state?.from !== undefined;
|
||||
|
||||
/**
|
||||
* Navigate back to the page we came from, falling back to a path when the
|
||||
* previous entry is not ours (deep link, login redirect, fresh tab).
|
||||
*/
|
||||
export const goBack = async (fallbackPath?: string): Promise<void> => {
|
||||
const canProceed = await ensureDialogsClosed(Date.now());
|
||||
@@ -110,14 +132,12 @@ export const goBack = async (fallbackPath?: string): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have history to go back to
|
||||
const { history } = mainWindow;
|
||||
if (history.length > 1) {
|
||||
history.back();
|
||||
// Read after closing dialogs: their history entries are popped by then, so
|
||||
// this is the state of the page entry.
|
||||
if (canGoBack()) {
|
||||
mainWindow.history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
// No history available, navigate to fallback path
|
||||
const fallback = fallbackPath || "/";
|
||||
navigate(fallback, { replace: true });
|
||||
navigate(fallbackPath || "/", { replace: true });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { mainWindow } from "../dom/get_main_window";
|
||||
|
||||
/**
|
||||
* The path of the page currently shown by the app. The demo routes with the
|
||||
* hash instead of the pathname, see navigate().
|
||||
*/
|
||||
export const currentPath = (): string =>
|
||||
__DEMO__
|
||||
? mainWindow.location.hash.substring(1)
|
||||
: mainWindow.location.pathname;
|
||||
@@ -18,6 +18,8 @@ export class HaProgressButton extends LitElement {
|
||||
|
||||
@property() appearance: Appearance = "accent";
|
||||
|
||||
@property() size: "xs" | "s" | "m" | "l" | "xl" = "m";
|
||||
|
||||
@property({ attribute: false }) public iconPath?: string;
|
||||
|
||||
@property() variant: "brand" | "danger" | "neutral" | "warning" | "success" =
|
||||
@@ -32,6 +34,7 @@ export class HaProgressButton extends LitElement {
|
||||
return html`
|
||||
<ha-button
|
||||
.appearance=${appearance}
|
||||
.size=${this.size}
|
||||
.disabled=${this.disabled}
|
||||
.loading=${this.progress}
|
||||
.variant=${
|
||||
@@ -118,6 +121,12 @@ export class HaProgressButton extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* The icon lives in this shadow root, so callers cannot size it themselves. */
|
||||
ha-button[size="xs"] ha-svg-icon[slot="start"],
|
||||
ha-button[size="s"] ha-svg-icon[slot="start"] {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
|
||||
/* Fade the content out rather than hiding it, so the button keeps its
|
||||
accessible name while the result icon covers it. */
|
||||
ha-button.result::part(start),
|
||||
|
||||
@@ -26,7 +26,10 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { getAllGraphColors } from "../../common/color/colors";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
@@ -1216,7 +1219,9 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
// Long-press to solo on touch/pen devices (500ms, consistent with action-handler-directive)
|
||||
private _legendPointerDown(ev: PointerEvent) {
|
||||
private _legendPointerDown(
|
||||
ev: PointerEvent & HASSDomCurrentTargetEvent<HTMLElement>
|
||||
) {
|
||||
// Mouse uses Ctrl/Cmd+click instead
|
||||
if (ev.pointerType === "mouse") {
|
||||
return;
|
||||
@@ -1246,7 +1251,9 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleDataset(ev: MouseEvent) {
|
||||
private _toggleDataset(
|
||||
ev: MouseEvent & HASSDomCurrentTargetEvent<HTMLElement>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
if (!this.chart) {
|
||||
return;
|
||||
@@ -1268,7 +1275,7 @@ export class HaChartBase extends LitElement {
|
||||
this._handleDatasetToggle(id);
|
||||
}
|
||||
|
||||
private _labelClick(ev: MouseEvent) {
|
||||
private _labelClick(ev: MouseEvent & HASSDomCurrentTargetEvent<HTMLElement>) {
|
||||
ev.stopPropagation();
|
||||
if (!this.chart) {
|
||||
return;
|
||||
|
||||
@@ -2,13 +2,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
|
||||
import type {
|
||||
CallbackDataParams,
|
||||
ECElementEvent,
|
||||
} from "echarts/types/src/util/types";
|
||||
import type { CallbackDataParams } from "echarts/types/src/util/types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import SankeyChart from "../../resources/echarts/components/sankey/install";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
@@ -121,11 +118,15 @@ export class HaSankeyChart extends LitElement {
|
||||
return null;
|
||||
};
|
||||
|
||||
private _handleChartSankeyRoam = (ev: CustomEvent) => {
|
||||
private _handleChartSankeyRoam = (
|
||||
ev: HASSDomEvent<HASSDomEvents["chart-sankeyroam"]>
|
||||
) => {
|
||||
this._currentZoom = ev.detail.zoom;
|
||||
};
|
||||
|
||||
private _handleChartClick = (ev: CustomEvent<ECElementEvent>) => {
|
||||
private _handleChartClick = (
|
||||
ev: HASSDomEvent<HASSDomEvents["chart-click"]>
|
||||
) => {
|
||||
const detail = ev.detail;
|
||||
// Only handle node clicks (not links)
|
||||
if (detail.dataType !== "node") {
|
||||
|
||||
@@ -216,11 +216,13 @@ export class StateHistoryChartLine extends LitElement {
|
||||
})}`;
|
||||
};
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
private _datasetHidden(ev: HASSDomEvent<HASSDomEvents["dataset-hidden"]>) {
|
||||
this._hiddenStats.add(ev.detail.id);
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev: CustomEvent) {
|
||||
private _datasetUnhidden(
|
||||
ev: HASSDomEvent<HASSDomEvents["dataset-unhidden"]>
|
||||
) {
|
||||
this._hiddenStats.delete(ev.detail.id);
|
||||
}
|
||||
|
||||
@@ -229,7 +231,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
chartBase.zoom(start, end, true);
|
||||
}
|
||||
|
||||
private _handleDataZoom(ev: CustomEvent) {
|
||||
private _handleDataZoom(ev: HASSDomEvent<HASSDomEvents["chart-zoom"]>) {
|
||||
fireEvent(this, "chart-zoom-with-index", {
|
||||
start: ev.detail.start ?? 0,
|
||||
end: ev.detail.end ?? 100,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import type {
|
||||
CustomSeriesOption,
|
||||
CustomSeriesRenderItem,
|
||||
ECElementEvent,
|
||||
TooltipPositionCallbackParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
@@ -21,7 +20,7 @@ import echarts from "../../resources/echarts/echarts";
|
||||
import { luminosity } from "../../common/color/rgb";
|
||||
import { hex2rgb } from "../../common/color/convert-color";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
|
||||
@customElement("state-history-chart-timeline")
|
||||
export class StateHistoryChartTimeline extends LitElement {
|
||||
@@ -271,7 +270,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
chartBase.zoom(start, end, true);
|
||||
}
|
||||
|
||||
private _handleDataZoom(ev: CustomEvent) {
|
||||
private _handleDataZoom(ev: HASSDomEvent<HASSDomEvents["chart-zoom"]>) {
|
||||
fireEvent(this, "chart-zoom-with-index", {
|
||||
start: ev.detail.start ?? 0,
|
||||
end: ev.detail.end ?? 100,
|
||||
@@ -385,7 +384,9 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
this._chartData = datasets;
|
||||
}
|
||||
|
||||
private _handleChartClick(e: CustomEvent<ECElementEvent>): void {
|
||||
private _handleChartClick(
|
||||
e: HASSDomEvent<HASSDomEvents["chart-click"]>
|
||||
): void {
|
||||
if (e.detail.targetType === "axisLabel") {
|
||||
const dataset = this._chartData[e.detail.dataIndex];
|
||||
if (dataset) {
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
} from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import type {
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HistoryResult,
|
||||
LineChartUnit,
|
||||
@@ -316,13 +320,13 @@ export class StateHistoryCharts extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _yWidthChanged(e: CustomEvent<HASSDomEvents["y-width-changed"]>) {
|
||||
private _yWidthChanged(e: HASSDomEvent<HASSDomEvents["y-width-changed"]>) {
|
||||
this._childYWidths[e.detail.chartIndex] = e.detail.value;
|
||||
this._maxYWidth = Math.max(...Object.values(this._childYWidths), 0);
|
||||
}
|
||||
|
||||
private _handleTimelineSync(
|
||||
e: CustomEvent<HASSDomEvents["chart-zoom-with-index"]>
|
||||
e: HASSDomEvent<HASSDomEvents["chart-zoom-with-index"]>
|
||||
) {
|
||||
if (!this.syncCharts || this._isSyncing) {
|
||||
return;
|
||||
@@ -384,7 +388,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _saveScrollPos(e: Event) {
|
||||
private _saveScrollPos(e: HASSDomTargetEvent<HTMLDivElement>) {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
|
||||
@@ -204,12 +204,14 @@ export class StatisticsChart extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
private _datasetHidden(ev: HASSDomEvent<HASSDomEvents["dataset-hidden"]>) {
|
||||
this._hiddenStats.add(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev: CustomEvent) {
|
||||
private _datasetUnhidden(
|
||||
ev: HASSDomEvent<HASSDomEvents["dataset-unhidden"]>
|
||||
) {
|
||||
this._hiddenStats.delete(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export class HaInputChip extends InputChip {
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
max-width: 100%;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--primary-text-color);
|
||||
|
||||
@@ -9,12 +9,16 @@ import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||
import {
|
||||
formatCallyDateRange,
|
||||
formatDateMonth,
|
||||
formatDateYear,
|
||||
formatCallyMonthYear,
|
||||
formatDateMonthYear,
|
||||
formatISODateOnly,
|
||||
} from "../../common/datetime/format_date";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import { configContext, internationalizationContext } from "../../data/context";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import { MobileAwareMixin } from "../../mixins/mobile-aware-mixin";
|
||||
@@ -56,13 +60,16 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
})
|
||||
private _hassConfig!: HassConfig;
|
||||
|
||||
/** used to show month in calendar-range header */
|
||||
@state() private _pickerMonth?: string;
|
||||
/** used to show month and year in calendar-range header */
|
||||
@state() private _pickerMonthYear?: string;
|
||||
|
||||
/** used to show year in calendar-date header */
|
||||
@state() private _pickerYear?: string;
|
||||
|
||||
/** used for today to navigate focus in calendar-range */
|
||||
/**
|
||||
* used for today to navigate focus in calendar-range
|
||||
*
|
||||
* Always mirrors the day calendar-range has focused, never cleared: once this
|
||||
* has held a date, rendering `undefined` over it makes cally fall back to the
|
||||
* selected range and page the calendar away from where the user is.
|
||||
*/
|
||||
@state() private _focusDate?: string;
|
||||
|
||||
@state() private _dateValue?: string;
|
||||
@@ -88,12 +95,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
this._hassConfig
|
||||
)
|
||||
: undefined;
|
||||
this._pickerMonth = formatDateMonth(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerYear = formatDateYear(
|
||||
this._pickerMonthYear = formatDateMonthYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
@@ -163,9 +165,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
slot="previous"
|
||||
></ha-icon-button-prev>
|
||||
<div class="heading" slot="heading">
|
||||
<span class="month-year"
|
||||
>${this._pickerMonth} ${this._pickerYear}</span
|
||||
>
|
||||
<span class="month-year">${this._pickerMonthYear}</span>
|
||||
<ha-icon-button
|
||||
@click=${this._focusToday}
|
||||
.path=${mdiCalendarToday}
|
||||
@@ -229,12 +229,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerMonth = formatDateMonth(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerYear = formatDateYear(
|
||||
this._pickerMonthYear = formatDateMonthYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
@@ -307,28 +302,27 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _focusChanged(ev: CustomEvent<Date>) {
|
||||
const date = ev.detail;
|
||||
this._pickerMonth = formatDateMonth(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerYear = formatDateYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._focusDate = undefined;
|
||||
private _focusChanged(
|
||||
ev: HASSDomEvent<Date> &
|
||||
HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
|
||||
) {
|
||||
this._pickerMonthYear = formatCallyMonthYear(ev.detail, this._i18n.locale);
|
||||
this._focusDate = ev.target.focusedDate;
|
||||
}
|
||||
|
||||
private _handleChange(ev: CustomEvent) {
|
||||
private _handleChange(
|
||||
ev: HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
|
||||
) {
|
||||
const dateElement = ev.target as HTMLElementTagNameMap["calendar-range"];
|
||||
this._dateValue = dateElement.value;
|
||||
this._focusDate = undefined;
|
||||
this._focusDate = dateElement.focusedDate;
|
||||
}
|
||||
|
||||
private _clickDateRangeChip(ev: Event) {
|
||||
private _clickDateRangeChip(
|
||||
ev: HASSDomTargetEvent<
|
||||
HaFilterChip & { index: number; range: [Date, Date] }
|
||||
>
|
||||
) {
|
||||
const chip = ev.target as HaFilterChip & {
|
||||
index: number;
|
||||
range: [Date, Date];
|
||||
@@ -336,7 +330,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
this._saveDateRangePreset(chip.range, chip.index);
|
||||
}
|
||||
|
||||
private _setDateRange(ev: CustomEvent<ActionDetail>) {
|
||||
private _setDateRange(ev: HASSDomEvent<ActionDetail>) {
|
||||
const dateRange: [Date, Date] = Object.values(this.ranges!)[
|
||||
ev.detail.index
|
||||
];
|
||||
@@ -355,7 +349,9 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleChangeTime(ev: ValueChangedEvent<string>) {
|
||||
private _handleChangeTime(
|
||||
ev: ValueChangedEvent<string> & HASSDomTargetEvent<HaBaseTimeInput>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const time = ev.detail.value;
|
||||
const target = ev.target as HaBaseTimeInput;
|
||||
|
||||
@@ -6,13 +6,18 @@ import type { HassConfig } from "home-assistant-js-websocket/dist/types";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import {
|
||||
formatDateMonth,
|
||||
formatCallyMonthYear,
|
||||
formatDateMonthYear,
|
||||
formatDateShort,
|
||||
formatDateYear,
|
||||
formatISODateOnly,
|
||||
} from "../../common/datetime/format_date";
|
||||
import { valueToDate } from "../../common/datetime/value_to_date";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import type {
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import { configContext, internationalizationContext } from "../../data/context";
|
||||
import { DialogMixin } from "../../dialogs/dialog-mixin";
|
||||
import type { HomeAssistantConfig } from "../../types";
|
||||
@@ -56,13 +61,16 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
dateString: string;
|
||||
};
|
||||
|
||||
/** used to show month in calendar-date header */
|
||||
@state() private _pickerMonth?: string;
|
||||
/** used to show month and year in calendar-date header */
|
||||
@state() private _pickerMonthYear?: string;
|
||||
|
||||
/** used to show year in calendar-date header */
|
||||
@state() private _pickerYear?: string;
|
||||
|
||||
/** used for today to navigate focus in cally-calendar-date */
|
||||
/**
|
||||
* used for today to navigate focus in cally-calendar-date
|
||||
*
|
||||
* Always mirrors the day calendar-date has focused, never cleared: once this
|
||||
* has held a date, rendering `undefined` over it makes cally fall back to the
|
||||
* selected value and page the calendar away from where the user is.
|
||||
*/
|
||||
@state() private _focusDate?: string;
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -71,12 +79,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
if (this.params) {
|
||||
const date = valueToDate(this.params.value);
|
||||
|
||||
this._pickerYear = formatDateYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerMonth = formatDateMonth(
|
||||
this._pickerMonthYear = formatDateMonthYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
@@ -84,7 +87,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
|
||||
this._value = this.params.value
|
||||
? {
|
||||
year: this._pickerYear,
|
||||
year: formatDateYear(date, this._i18n.locale, this._hassConfig),
|
||||
title: formatDateShort(date, this._i18n.locale, this._hassConfig),
|
||||
dateString: formatISODateOnly(
|
||||
date,
|
||||
@@ -140,9 +143,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
slot="previous"
|
||||
></ha-icon-button-prev>
|
||||
<div class="heading" slot="heading">
|
||||
<span class="month-year"
|
||||
>${this._pickerMonth} ${this._pickerYear}</span
|
||||
>
|
||||
<span class="month-year">${this._pickerMonthYear}</span>
|
||||
<ha-icon-button
|
||||
@click=${this._setToday}
|
||||
.path=${mdiCalendarToday}
|
||||
@@ -167,7 +168,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
</ha-adaptive-popover>`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event) {
|
||||
private _valueChanged(ev: HASSDomTargetEvent<CalendarDate>) {
|
||||
const dateElement = ev.target as CalendarDate;
|
||||
if (dateElement.value) {
|
||||
this._updateValue(dateElement.value);
|
||||
@@ -185,12 +186,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
|
||||
if (setFocusDay) {
|
||||
this._focusDate = this._value.dateString;
|
||||
this._pickerMonth = formatDateMonth(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerYear = formatDateYear(
|
||||
this._pickerMonthYear = formatDateMonthYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
@@ -198,19 +194,11 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
}
|
||||
}
|
||||
|
||||
private _focusChanged(ev: CustomEvent<Date>) {
|
||||
const date = ev.detail;
|
||||
this._pickerMonth = formatDateMonth(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._pickerYear = formatDateYear(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
this._focusDate = undefined;
|
||||
private _focusChanged(
|
||||
ev: HASSDomEvent<Date> & HASSDomTargetEvent<CalendarDate>
|
||||
) {
|
||||
this._pickerMonthYear = formatCallyMonthYear(ev.detail, this._i18n.locale);
|
||||
this._focusDate = ev.target.focusedDate;
|
||||
}
|
||||
|
||||
private _clear() {
|
||||
|
||||
@@ -7,6 +7,10 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
@@ -191,7 +195,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _floorMoved(ev: CustomEvent<HASSDomEvents["item-moved"]>) {
|
||||
private _floorMoved(ev: HASSDomEvent<HASSDomEvents["item-moved"]>) {
|
||||
ev.stopPropagation();
|
||||
const newIndex = ev.detail.newIndex;
|
||||
const oldIndex = ev.detail.oldIndex;
|
||||
@@ -215,7 +219,12 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private async _areaDisplayChanged(ev: ValueChangedEvent<DisplayValue>) {
|
||||
private async _areaDisplayChanged(
|
||||
ev: ValueChangedEvent<DisplayValue> &
|
||||
HASSDomCurrentTargetEvent<
|
||||
HTMLElementTagNameMap["ha-items-display-editor"] & { floorId?: string }
|
||||
>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
const currentFloorId = (ev.currentTarget as any).floorId;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import type {
|
||||
@@ -43,7 +44,7 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event) {
|
||||
private _valueChanged(ev: HASSDomTargetEvent<HaCheckbox>) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: (ev.target as HaCheckbox).checked,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HaFormDividerSchema, HaFormElement } from "./types";
|
||||
|
||||
@customElement("ha-form-divider")
|
||||
export class HaFormDivider extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public schema!: HaFormDividerSchema;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<hr />`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-form-divider": HaFormDivider;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../input/ha-input";
|
||||
import type { HaInput } from "../input/ha-input";
|
||||
@@ -70,7 +71,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInput(ev: InputEvent) {
|
||||
private _handleInput(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
|
||||
const source = ev.target as HaInput;
|
||||
const rawValue = (source.value ?? "").replace(",", ".");
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
@@ -153,7 +154,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _handleCheckboxChange(ev: Event) {
|
||||
private _handleCheckboxChange(ev: HASSDomTargetEvent<HaCheckbox>) {
|
||||
const checked = (ev.target as HaCheckbox).checked;
|
||||
let value: HaFormIntegerData | undefined;
|
||||
if (checked) {
|
||||
@@ -178,7 +179,9 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(ev: InputEvent) {
|
||||
private _valueChanged(
|
||||
ev: InputEvent & HASSDomTargetEvent<HaInput | HTMLInputElement>
|
||||
) {
|
||||
const source = ev.target as HaInput | HTMLInputElement;
|
||||
const rawValue = source.value;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-check-list-item";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
@@ -140,7 +141,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
private _valueChanged(ev: HASSDomTargetEvent<HaCheckbox>): void {
|
||||
const { value, checked } = ev.target as HaCheckbox;
|
||||
this._handleValueChanged(value, checked);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
import "../ha-dropdown-item";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-form";
|
||||
import "./ha-form-divider";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
HaFormDataContainer,
|
||||
HaFormDividerSchema,
|
||||
HaFormElement,
|
||||
HaFormOptionalActionsSchema,
|
||||
HaFormSchema,
|
||||
@@ -22,6 +24,11 @@ import type {
|
||||
|
||||
const NO_ACTIONS = [];
|
||||
|
||||
const DIVIDER = {
|
||||
name: "",
|
||||
type: "divider",
|
||||
} as const satisfies HaFormDividerSchema;
|
||||
|
||||
@customElement("ha-form-optional_actions")
|
||||
export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
@@ -87,7 +94,9 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
schema: readonly HaFormSchema[],
|
||||
displayActions: string[]
|
||||
): HaFormSchema[] =>
|
||||
schema.filter((item) => displayActions.includes(item.name))
|
||||
schema
|
||||
.filter((item) => displayActions.includes(item.name))
|
||||
.flatMap((item) => [DIVIDER, item])
|
||||
);
|
||||
|
||||
public render(): TemplateResult {
|
||||
@@ -128,6 +137,7 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
${
|
||||
hiddenActions.length > 0
|
||||
? html`
|
||||
<ha-form-divider></ha-form-divider>
|
||||
<ha-dropdown
|
||||
@wa-select=${this._handleAddAction}
|
||||
@closed=${stopPropagation}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { SelectSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-selector/ha-selector-select";
|
||||
import type {
|
||||
HaFormElement,
|
||||
@@ -78,7 +78,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
let value: HaFormSelectData | undefined = ev.detail.value;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-icon-button";
|
||||
import "../input/ha-input";
|
||||
@@ -79,7 +80,7 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected _valueChanged(ev: Event): void {
|
||||
protected _valueChanged(ev: HASSDomTargetEvent<HaInput>): void {
|
||||
let value: string | undefined = (ev.target as HaInput).value;
|
||||
if (this.data === value) {
|
||||
return;
|
||||
|
||||
@@ -3,15 +3,24 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-selector/ha-selector";
|
||||
import { getHiddenFields } from "./conditions";
|
||||
import type { HaFormDataContainer, HaFormElement, HaFormSchema } from "./types";
|
||||
import type {
|
||||
HaFormData,
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
|
||||
type HaFormDataChangedEvent = ValueChangedEvent<HaFormData>;
|
||||
type HaFormDataContainerChangedEvent = ValueChangedEvent<HaFormDataContainer>;
|
||||
|
||||
const LOAD_ELEMENTS = {
|
||||
boolean: () => import("./ha-form-boolean"),
|
||||
constant: () => import("./ha-form-constant"),
|
||||
divider: () => import("./ha-form-divider"),
|
||||
float: () => import("./ha-form-float"),
|
||||
grid: () => import("./ha-form-grid"),
|
||||
expandable: () => import("./ha-form-expandable"),
|
||||
@@ -261,16 +270,18 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected addValueChangedListener(element: Element | ShadowRoot) {
|
||||
element.addEventListener("value-changed", (ev) => {
|
||||
element.addEventListener("value-changed", (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
|
||||
|
||||
if (ev.target === this) return;
|
||||
|
||||
const changeEv = ev as
|
||||
HaFormDataChangedEvent | HaFormDataContainerChangedEvent;
|
||||
const newValue =
|
||||
!schema.name || ("flatten" in schema && schema.flatten)
|
||||
? ev.detail.value
|
||||
: { [schema.name]: ev.detail.value };
|
||||
? (changeEv.detail.value as HaFormDataContainer)
|
||||
: { [schema.name]: changeEv.detail.value as HaFormData };
|
||||
|
||||
this.data = {
|
||||
...this.data,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { HaDurationData } from "../ha-duration-input";
|
||||
|
||||
export type HaFormSchema =
|
||||
| HaFormConstantSchema
|
||||
| HaFormDividerSchema
|
||||
| HaFormStringSchema
|
||||
| HaFormIntegerSchema
|
||||
| HaFormFloatSchema
|
||||
@@ -97,6 +98,10 @@ export interface HaFormConstantSchema extends HaFormBaseSchema {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface HaFormDividerSchema extends HaFormBaseSchema {
|
||||
type: "divider";
|
||||
}
|
||||
|
||||
export interface HaFormIntegerSchema extends HaFormBaseSchema {
|
||||
type: "integer";
|
||||
default?: HaFormIntegerData;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../common/dom/fire_event";
|
||||
import type {
|
||||
Adapter,
|
||||
IPv4ConfiguredAddress,
|
||||
@@ -101,7 +102,9 @@ export class HaNetwork extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAutoConfigureCheckboxClick(ev: Event) {
|
||||
private _handleAutoConfigureCheckboxClick(
|
||||
ev: HASSDomCurrentTargetEvent<HaCheckbox>
|
||||
) {
|
||||
const checkbox = ev.currentTarget as HaCheckbox;
|
||||
if (this.networkConfig === undefined) {
|
||||
return;
|
||||
@@ -127,7 +130,9 @@ export class HaNetwork extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleAdapterCheckboxClick(ev: Event) {
|
||||
private _handleAdapterCheckboxClick(
|
||||
ev: HASSDomCurrentTargetEvent<HaCheckbox>
|
||||
) {
|
||||
const checkbox = ev.currentTarget as HaCheckbox;
|
||||
const adapter_name = checkbox.id;
|
||||
if (this.networkConfig === undefined) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getAppKey } from "../data/notify_html5";
|
||||
import { showPromptDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import type { HaSwitch } from "./ha-switch";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent, type HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import "./ha-switch";
|
||||
|
||||
export const pushSupported =
|
||||
@@ -55,7 +55,7 @@ class HaPushNotificationsToggle extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePushChange(ev: Event) {
|
||||
private _handlePushChange(ev: HASSDomTargetEvent<HaSwitch>) {
|
||||
// Somehow this is triggered on Safari on page load causing
|
||||
// it to get into a loop and crash the page.
|
||||
if (!pushSupported) return;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { customElement, property } 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 type { HASSDomCurrentTargetEvent } from "../common/dom/fire_event";
|
||||
import "./ha-tooltip";
|
||||
|
||||
export interface Segment {
|
||||
@@ -120,7 +121,9 @@ class HaSegmentedBar extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSegmentClick(ev: Event): void {
|
||||
private _handleSegmentClick(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement>
|
||||
): void {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
const index = Number(target.dataset.index);
|
||||
const segment = this.segments[index];
|
||||
@@ -129,7 +132,7 @@ class HaSegmentedBar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleLegendClick(ev: Event): void {
|
||||
private _handleLegendClick(ev: HASSDomCurrentTargetEvent<HTMLElement>): void {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
const index = Number(target.dataset.index);
|
||||
const segment = this.segments[index];
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-picker-field";
|
||||
@@ -169,7 +170,7 @@ export class HaSelect extends LitElement {
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _handleSelect(ev: CustomEvent<{ item: { value: string | number } }>) {
|
||||
private _handleSelect(ev: HaDropdownSelectEvent<string | number>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.item.value;
|
||||
if (value === this.value) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import { getConfigEntries } from "../../data/config_entries";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import type { EntitySources } from "../../data/entity/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
|
||||
import type { EntitySelector } from "../../data/selector";
|
||||
@@ -42,10 +41,6 @@ export class HaEntitySelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ attribute: false }) public context?: {
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
};
|
||||
|
||||
@state() private _createDomains: string[] | undefined;
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(
|
||||
@@ -174,9 +169,6 @@ export class HaEntitySelector extends LitElement {
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (this.context?.entityFilter && !this.context.entityFilter(entity)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.selector?.entity?.filter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@ export class HaSelectSelector extends LitElement {
|
||||
.idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
.label=${label}
|
||||
.title=${label}
|
||||
selected
|
||||
>
|
||||
${
|
||||
|
||||
@@ -17,7 +17,7 @@ import { stringCompare } from "../common/string/compare";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
|
||||
import type { ActionHandlerEvent } from "../data/lovelace/action_handler";
|
||||
import {
|
||||
FIXED_PANELS,
|
||||
getDefaultPanelUrlPath,
|
||||
@@ -634,7 +634,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
if (ev.detail.action !== "hold") {
|
||||
return;
|
||||
}
|
||||
@@ -646,7 +646,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
fireEvent(this, "hass-show-notifications");
|
||||
}
|
||||
|
||||
private _toggleSidebar(ev: CustomEvent) {
|
||||
private _toggleSidebar(ev: ActionHandlerEvent) {
|
||||
if (ev.detail.action !== "tap") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { HASSDomCurrentTargetEvent } from "../common/dom/fire_event";
|
||||
import { goBack } from "../common/navigate";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-icon-button-arrow-prev";
|
||||
@@ -324,7 +325,9 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
this._subRowResizeObserver = undefined;
|
||||
}
|
||||
|
||||
private _subRowSlotChanged = (ev: Event) => {
|
||||
private _subRowSlotChanged = (
|
||||
ev: HASSDomCurrentTargetEvent<HTMLSlotElement>
|
||||
) => {
|
||||
const slot = ev.currentTarget as HTMLSlotElement;
|
||||
this._hasSubRow = slot
|
||||
.assignedNodes({ flatten: true })
|
||||
|
||||
@@ -2,6 +2,7 @@ import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../common/util/copy-clipboard";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import { showToast } from "../../util/toast";
|
||||
@@ -111,7 +112,7 @@ export class HaInputCopy extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _focusInput(ev: Event) {
|
||||
private _focusInput(ev: HASSDomCurrentTargetEvent<HaInput>) {
|
||||
const inputElement = ev.currentTarget as HaInput;
|
||||
inputElement.select();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
|
||||
|
||||
/**
|
||||
* @element ha-row-item
|
||||
@@ -56,7 +57,7 @@ export class HaRowItem extends LitElement {
|
||||
@state() private _hasEnd = false;
|
||||
|
||||
private _onSlotChange(name: "start" | "end") {
|
||||
return (ev: Event) => {
|
||||
return (ev: HASSDomTargetEvent<HTMLSlotElement>) => {
|
||||
const slot = ev.target as HTMLSlotElement;
|
||||
const hasContent = slot
|
||||
.assignedNodes({ flatten: true })
|
||||
|
||||
@@ -175,6 +175,8 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private _pauseAutoFit = false;
|
||||
|
||||
private _pendingFit?: () => void;
|
||||
|
||||
public connectedCallback(): void {
|
||||
this._pauseAutoFit = false;
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
@@ -204,6 +206,7 @@ export class HaMap extends ReactiveElement {
|
||||
this.Leaflet = undefined;
|
||||
}
|
||||
|
||||
this._pendingFit = undefined;
|
||||
this._loaded = false;
|
||||
|
||||
if (this._resizeObserver) {
|
||||
@@ -347,6 +350,10 @@ export class HaMap extends ReactiveElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._deferIfUnsized(() => this.fitMap(options))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this._mapFocusItems.length &&
|
||||
!this._mapFocusZones.length &&
|
||||
@@ -387,6 +394,40 @@ export class HaMap extends ReactiveElement {
|
||||
}, PROGRAMMITIC_FIT_DELAY);
|
||||
}
|
||||
|
||||
// Leaflet derives the zoom level that fits given bounds from the current
|
||||
// size of the map container. When the container has not been laid out yet,
|
||||
// that size is 0x0 and the computed zoom collapses to the minimum, leaving
|
||||
// the map zoomed out to the world even after the container gets its size.
|
||||
// Defer fitting until the resize observer reports a usable size.
|
||||
private _deferIfUnsized(fit: () => void): boolean {
|
||||
const size = this.leafletMap!.getSize();
|
||||
if (size.x > 0 && size.y > 0) {
|
||||
this._pendingFit = undefined;
|
||||
return false;
|
||||
}
|
||||
const container = this.leafletMap!.getContainer();
|
||||
if (container.clientWidth > 0 && container.clientHeight > 0) {
|
||||
// The container was laid out since Leaflet last measured it.
|
||||
this.leafletMap!.invalidateSize(false);
|
||||
this._pendingFit = undefined;
|
||||
return false;
|
||||
}
|
||||
this._pendingFit = fit;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _runPendingFit(): void {
|
||||
if (!this._pendingFit || !this.leafletMap) {
|
||||
return;
|
||||
}
|
||||
const size = this.leafletMap.getSize();
|
||||
if (size.x > 0 && size.y > 0) {
|
||||
const pendingFit = this._pendingFit;
|
||||
this._pendingFit = undefined;
|
||||
pendingFit();
|
||||
}
|
||||
}
|
||||
|
||||
public fitBounds(
|
||||
boundingbox: LatLngExpression[],
|
||||
options?: { zoom?: number; pad?: number }
|
||||
@@ -394,6 +435,9 @@ export class HaMap extends ReactiveElement {
|
||||
if (!this.leafletMap || !this.Leaflet) {
|
||||
return;
|
||||
}
|
||||
if (this._deferIfUnsized(() => this.fitBounds(boundingbox, options))) {
|
||||
return;
|
||||
}
|
||||
const bounds = this.Leaflet.latLngBounds(boundingbox).pad(
|
||||
options?.pad ?? 0.5
|
||||
);
|
||||
@@ -773,6 +817,7 @@ export class HaMap extends ReactiveElement {
|
||||
if (!this._resizeObserver) {
|
||||
this._resizeObserver = new ResizeObserver(() => {
|
||||
this.leafletMap?.invalidateSize({ debounceMoveend: true });
|
||||
this._runPendingFit();
|
||||
});
|
||||
}
|
||||
this._resizeObserver.observe(this);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { css, html, LitElement, nothing, svg } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
|
||||
import { BRANCH_HEIGHT, SPACING } from "./hat-graph-const";
|
||||
|
||||
interface BranchConfig {
|
||||
@@ -31,7 +32,7 @@ export class HatGraphBranch extends LitElement {
|
||||
|
||||
private _maxHeight = 0;
|
||||
|
||||
private _updateBranches(ev: Event) {
|
||||
private _updateBranches(ev: HASSDomTargetEvent<HTMLSlotElement>) {
|
||||
let total_width = 0;
|
||||
const heights: number[] = [];
|
||||
const branches: BranchConfig[] = [];
|
||||
|
||||
@@ -14,7 +14,7 @@ declare global {
|
||||
}
|
||||
|
||||
interface GlobalEventHandlersEventMap {
|
||||
"connection-status": HASSDomEvent<ConnectionStatus>;
|
||||
"connection-status": HASSDomEvent<HASSDomEvents["connection-status"]>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ declare global {
|
||||
"hass-related-context": RelatedContextItem | undefined;
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"hass-related-context": HASSDomEvent<RelatedContextItem | undefined>;
|
||||
"hass-related-context": HASSDomEvent<HASSDomEvents["hass-related-context"]>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-2
@@ -793,6 +793,17 @@ const findEnergyDataCollection = (
|
||||
return (hass.connection as any)[key];
|
||||
};
|
||||
|
||||
// The last-picked preset is remembered per energy collection, so each dashboard
|
||||
// reopens on its own default period. Derived from the connection key so the read
|
||||
// and write sides cannot drift apart.
|
||||
export const getEnergyDefaultPeriodStorageKey = (
|
||||
hass: HomeAssistant,
|
||||
collectionKey?: string
|
||||
): string => {
|
||||
const [key] = convertCollectionKeyToConnection(hass, collectionKey);
|
||||
return `energy-default-period-${key}`;
|
||||
};
|
||||
|
||||
// When does the collection's day period need to roll over to the next day?
|
||||
// With `midnightRollover` (the real-time "Now" view) it rolls over right at
|
||||
// midnight. Otherwise it waits an hour, until the new day's first hourly
|
||||
@@ -892,8 +903,9 @@ export const getEnergyDataCollection = (
|
||||
// The real-time "Now" view always tracks today; it shows live data even
|
||||
// before today's first statistic exists, so it never falls back to yesterday.
|
||||
const preferredPeriod =
|
||||
(localStorage.getItem(`energy-default-period-${key}`) as DateRange) ||
|
||||
"today";
|
||||
(localStorage.getItem(
|
||||
getEnergyDefaultPeriodStorageKey(hass, options.key)
|
||||
) as DateRange) || "today";
|
||||
const period =
|
||||
preferredPeriod === "today" && hour === "0" && !midnightRollover
|
||||
? "yesterday"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { Condition } from "../panels/lovelace/common/validate-condition";
|
||||
import type { ShortcutItem } from "./home_shortcuts";
|
||||
|
||||
export interface SurveyInteraction {
|
||||
@@ -36,17 +35,6 @@ export interface HomeFrontendSystemData {
|
||||
shortcuts?: ShortcutItem[];
|
||||
}
|
||||
|
||||
export interface SecurityAlertEntityConfig {
|
||||
entity: string;
|
||||
color?: string;
|
||||
pulse?: boolean;
|
||||
visibility?: Condition[];
|
||||
}
|
||||
|
||||
export interface SecurityFrontendSystemData {
|
||||
alert_entities?: SecurityAlertEntityConfig[];
|
||||
}
|
||||
|
||||
export interface EnergyFrontendSystemData {
|
||||
// Stable "<view>.<card-type>" keys of energy dashboard cards the user has
|
||||
// hidden. An absent key or array means nothing is hidden (all cards visible),
|
||||
@@ -63,7 +51,6 @@ declare global {
|
||||
core: CoreFrontendSystemData;
|
||||
home: HomeFrontendSystemData;
|
||||
energy: EnergyFrontendSystemData;
|
||||
security: SecurityFrontendSystemData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ declare global {
|
||||
}
|
||||
|
||||
interface GlobalEventHandlersEventMap {
|
||||
haptic: HASSDomEvent<HapticType>;
|
||||
haptic: HASSDomEvent<HASSDomEvents["haptic"]>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-formfield";
|
||||
@@ -161,18 +162,18 @@ class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptio
|
||||
`;
|
||||
}
|
||||
|
||||
private _disableNewEntitiesChanged(ev: Event): void {
|
||||
private _disableNewEntitiesChanged(ev: HASSDomTargetEvent<HaSwitch>): void {
|
||||
this._error = undefined;
|
||||
this._disableNewEntities = !(ev.target as HaSwitch).checked;
|
||||
this._disableNewEntities = !ev.target.checked;
|
||||
this._updateDirtyState({
|
||||
disableNewEntities: this._disableNewEntities,
|
||||
disablePolling: this._disablePolling,
|
||||
});
|
||||
}
|
||||
|
||||
private _disablePollingChanged(ev: Event): void {
|
||||
private _disablePollingChanged(ev: HASSDomTargetEvent<HaSwitch>): void {
|
||||
this._error = undefined;
|
||||
this._disablePolling = !(ev.target as HaSwitch).checked;
|
||||
this._disablePolling = !ev.target.checked;
|
||||
this._updateDirtyState({
|
||||
disableNewEntities: this._disableNewEntities,
|
||||
disablePolling: this._disablePolling,
|
||||
|
||||
@@ -70,8 +70,10 @@ declare global {
|
||||
}
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
"flow-update": HASSDomEvent<FlowUpdateEvent>;
|
||||
"flow-step-footer-state-changed": HASSDomEvent<FlowStepFooterStateChangedEvent>;
|
||||
"flow-update": HASSDomEvent<HASSDomEvents["flow-update"]>;
|
||||
"flow-step-footer-state-changed": HASSDomEvent<
|
||||
HASSDomEvents["flow-step-footer-state-changed"]
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,7 +743,7 @@ class DataEntryFlowDialog extends DirtyStateProviderMixin<
|
||||
};
|
||||
|
||||
private _handleFooterStateChanged = (
|
||||
ev: HASSDomEvent<FlowStepFooterStateChangedEvent>
|
||||
ev: HASSDomEvent<HASSDomEvents["flow-step-footer-state-changed"]>
|
||||
) => {
|
||||
if (ev.detail.loading !== undefined) {
|
||||
this._formStepLoading = ev.detail.loading;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../../common/dom/is-navigation-click";
|
||||
import "../../components/ha-alert";
|
||||
import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data";
|
||||
@@ -153,7 +153,7 @@ class StepFlowForm extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _setError(ev: CustomEvent) {
|
||||
private _setError(ev: HASSDomEvent<DataEntryFlowStepForm["errors"]>) {
|
||||
this._previewErrors = ev.detail;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import { consume } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import type { HaProgressButton } from "../../../components/buttons/ha-progress-button";
|
||||
import "../../../components/ha-camera-stream";
|
||||
import type { CameraEntity } from "../../../data/camera";
|
||||
import { apiContext } from "../../../data/context";
|
||||
@@ -66,8 +68,10 @@ class MoreInfoCamera extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _downloadSnapshot(ev: CustomEvent) {
|
||||
const button = ev.currentTarget as any;
|
||||
private async _downloadSnapshot(
|
||||
ev: HASSDomCurrentTargetEvent<HaProgressButton>
|
||||
) {
|
||||
const button = ev.currentTarget;
|
||||
this._waiting = true;
|
||||
|
||||
try {
|
||||
|
||||
@@ -3,8 +3,10 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-button";
|
||||
import type { HaButton } from "../../../components/ha-button";
|
||||
import { apiContext } from "../../../data/context";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistantApi } from "../../../types";
|
||||
@@ -67,8 +69,10 @@ class MoreInfoCounter extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleActionClick(e: MouseEvent): void {
|
||||
const action = (e.currentTarget as any).action;
|
||||
private _handleActionClick(
|
||||
e: MouseEvent & HASSDomCurrentTargetEvent<HaButton & { action: string }>
|
||||
): void {
|
||||
const action = e.currentTarget.action;
|
||||
this._api.callService("counter", action, {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
@@ -872,12 +873,14 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleMediaSeekChanged(e: Event): Promise<void> {
|
||||
private async _handleMediaSeekChanged(
|
||||
e: HASSDomTargetEvent<HaSlider>
|
||||
): Promise<void> {
|
||||
if (!this.stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = (e.target as any).value;
|
||||
const newValue = e.target.value;
|
||||
this._api.callService("media_player", "media_seek", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
seek_position: newValue,
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
hasRequiredScriptFields,
|
||||
requiredScriptFieldsFilled,
|
||||
} from "../../../data/script";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import "../components/ha-more-info-state-header";
|
||||
|
||||
@customElement("more-info-script")
|
||||
@@ -209,7 +209,9 @@ class MoreInfoScript extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _scriptDataChanged(ev: CustomEvent): void {
|
||||
private _scriptDataChanged(
|
||||
ev: ValueChangedEvent<Record<string, unknown>>
|
||||
): void {
|
||||
this._scriptData = { ...this._scriptData, ...ev.detail.value };
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-button";
|
||||
import type { HaButton } from "../../../components/ha-button";
|
||||
import "../../../components/ha-duration-input";
|
||||
import type { HaDurationData } from "../../../components/ha-duration-input";
|
||||
import { apiContext } from "../../../data/context";
|
||||
@@ -137,8 +139,10 @@ class MoreInfoTimer extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleActionClick(e: MouseEvent): void {
|
||||
const action = (e.currentTarget as any).action;
|
||||
private _handleActionClick(
|
||||
e: MouseEvent & HASSDomCurrentTargetEvent<HaButton & { action: string }>
|
||||
): void {
|
||||
const action = e.currentTarget.action;
|
||||
this._api.callService("timer", action, {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
getEntityEntryContext,
|
||||
} from "../../common/entity/context/get_entity_context";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { navigate, updateHistoryState } from "../../common/navigate";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { withViewTransition } from "../../common/util/view-transition";
|
||||
@@ -268,16 +268,12 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
}
|
||||
|
||||
private _setView(view: MoreInfoView) {
|
||||
history.replaceState(
|
||||
{
|
||||
...history.state,
|
||||
dialogParams: {
|
||||
...history.state?.dialogParams,
|
||||
view,
|
||||
},
|
||||
updateHistoryState({
|
||||
dialogParams: {
|
||||
...history.state?.dialogParams,
|
||||
view,
|
||||
},
|
||||
""
|
||||
);
|
||||
});
|
||||
this._currView = view;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/animation/ha-fade-in";
|
||||
import "../../components/ha-adaptive-dialog";
|
||||
import "../../components/ha-alert";
|
||||
@@ -334,13 +335,18 @@ class DialogRestart extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private async _handleAction(ev: Event) {
|
||||
private async _handleAction(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HaListItemButton & {
|
||||
action: "restart" | "reboot" | "shutdown" | "restart-safe-mode";
|
||||
}
|
||||
>
|
||||
) {
|
||||
if (this._loadingBackupInfo) {
|
||||
return;
|
||||
}
|
||||
this._loadingBackupInfo = true;
|
||||
const action = (ev.currentTarget as HaListItemButton & { action: string })
|
||||
.action as "restart" | "reboot" | "shutdown" | "restart-safe-mode";
|
||||
const action = ev.currentTarget.action;
|
||||
|
||||
const backupState = await this._loadBackupState();
|
||||
|
||||
@@ -369,7 +375,7 @@ class DialogRestart extends LitElement {
|
||||
|
||||
this._dialogOpen = false;
|
||||
|
||||
let actionFunc;
|
||||
let actionFunc: () => Promise<void>;
|
||||
|
||||
if (["restart", "restart-safe-mode"].includes(action)) {
|
||||
const serviceData =
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { formatLanguageCode } from "../../common/language/format_language";
|
||||
import "../../components/chips/ha-assist-chip";
|
||||
@@ -18,7 +18,7 @@ import { getLanguageScores } from "../../data/conversation";
|
||||
import { UNAVAILABLE } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { VoiceAssistantSetupDialogParams } from "./show-voice-assistant-setup-dialog";
|
||||
import "./voice-assistant-setup-step-area";
|
||||
import "./voice-assistant-setup-step-change-wake-word";
|
||||
@@ -337,7 +337,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
this._language = ev.detail.item.value;
|
||||
}
|
||||
|
||||
private _languageChanged(ev: CustomEvent) {
|
||||
private _languageChanged(ev: ValueChangedEvent<string | undefined>) {
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
@@ -351,7 +351,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
this._step = this._previousSteps.pop()!;
|
||||
}
|
||||
|
||||
private async _goToNextStep(ev?: CustomEvent) {
|
||||
private async _goToNextStep(ev?: HASSDomEvent<HASSDomEvents["next-step"]>) {
|
||||
if (ev?.detail?.updateConfig) {
|
||||
await this._fetchAssistConfiguration();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
|
||||
import "../../components/list/ha-list-base";
|
||||
@@ -50,14 +51,14 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
||||
</ha-list-base>`;
|
||||
}
|
||||
|
||||
private async _wakeWordPicked(ev: Event) {
|
||||
private async _wakeWordPicked(
|
||||
ev: HASSDomCurrentTargetEvent<HaListItemButton & { value: string }>
|
||||
) {
|
||||
if (!this.assistEntityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wakeWordId = (
|
||||
ev.currentTarget as HaListItemButton & { value: string }
|
||||
).value;
|
||||
const wakeWordId = ev.currentTarget.value;
|
||||
|
||||
await setWakeWords(this.hass, this.assistEntityId, [wakeWordId]);
|
||||
this._nextStep();
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { LanguageScore, LanguageScores } from "../../data/conversation";
|
||||
import { getLanguageScores, listAgents } from "../../data/conversation";
|
||||
import { listSTTEngines } from "../../data/stt";
|
||||
import { listTTSEngines, listTTSVoices } from "../../data/tts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
import { STEP } from "./voice-assistant-setup-dialog";
|
||||
@@ -265,8 +265,8 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
}
|
||||
|
||||
private async _createCloudPipeline(useLanguage: boolean): Promise<boolean> {
|
||||
let cloudTtsEntityId;
|
||||
let cloudSttEntityId;
|
||||
let cloudTtsEntityId: string | undefined;
|
||||
let cloudSttEntityId: string | undefined;
|
||||
for (const entity of Object.values(this.hass.entities)) {
|
||||
if (entity.platform === "cloud") {
|
||||
const domain = computeDomain(entity.entity_id);
|
||||
@@ -327,7 +327,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
|
||||
const ttsVoices = await listTTSVoices(
|
||||
this.hass,
|
||||
cloudTtsEntityId,
|
||||
cloudTtsEntityId!,
|
||||
ttsEngine.supported_languages[0]
|
||||
);
|
||||
|
||||
@@ -357,9 +357,9 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
language: (this.language || this.hass.config.language).split("-")[0],
|
||||
conversation_engine: "conversation.home_assistant",
|
||||
conversation_language: agent.supported_languages[0],
|
||||
stt_engine: cloudSttEntityId,
|
||||
stt_engine: cloudSttEntityId!,
|
||||
stt_language: sttEngine.supported_languages[0],
|
||||
tts_engine: cloudTtsEntityId,
|
||||
tts_engine: cloudTtsEntityId!,
|
||||
tts_language: ttsEngine.supported_languages[0],
|
||||
tts_voice: ttsVoices.voices![0].voice_id,
|
||||
wake_word_entity: null,
|
||||
@@ -380,7 +380,9 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
private _valueChanged(
|
||||
ev: ValueChangedEvent<(typeof OPTIONS)[number] | undefined>
|
||||
) {
|
||||
this._value = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -410,7 +412,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
fireEvent(this, "next-step", { step: STEP.LOCAL, option: this._value });
|
||||
}
|
||||
|
||||
private _languageChanged(ev: CustomEvent) {
|
||||
private _languageChanged(ev: ValueChangedEvent<string | undefined>) {
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { CSSResult } from "lit";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
@@ -7,6 +8,7 @@ import { navigate } from "../common/navigate";
|
||||
import type { CustomPanelInfo } from "../data/panel_custom";
|
||||
import { baseEntrypointStyles } from "../resources/styles";
|
||||
import { createCustomPanelElement } from "../util/custom-panel/create-custom-panel-element";
|
||||
import { dropRealmCollections } from "../util/custom-panel/drop-realm-collections";
|
||||
import { loadCustomPanel } from "../util/custom-panel/load-custom-panel";
|
||||
import { setCustomPanelProperties } from "../util/custom-panel/set-custom-panel-properties";
|
||||
|
||||
@@ -29,8 +31,14 @@ window.loadES5Adapter = () => {
|
||||
|
||||
let panelEl: HTMLElement | undefined;
|
||||
let initialized = false;
|
||||
// Kept so we can clean up after ourselves on pagehide without depending on
|
||||
// `window.parent.customPanel`, which `_cleanupPanel()` may already have deleted.
|
||||
let connection: Connection | undefined;
|
||||
|
||||
function setProperties(properties) {
|
||||
if (properties.hass?.connection) {
|
||||
connection = properties.hass.connection;
|
||||
}
|
||||
if (!panelEl) {
|
||||
return;
|
||||
}
|
||||
@@ -150,4 +158,9 @@ window.addEventListener("pagehide", () => {
|
||||
while (document.body.lastChild) {
|
||||
document.body.removeChild(document.body.lastChild);
|
||||
}
|
||||
// The connection is owned by the main window and outlives this realm, so any
|
||||
// collection we created on it has to go with us.
|
||||
if (connection) {
|
||||
dropRealmCollections(connection);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -121,6 +121,14 @@ declare global {
|
||||
}
|
||||
|
||||
interface GlobalEventHandlersEventMap {
|
||||
"matter-commission-finish": HASSDomEvent<MatterCommissionFinish>;
|
||||
"improv-discovered-device": HASSDomEvent<
|
||||
HASSDomEvents["improv-discovered-device"]
|
||||
>;
|
||||
"improv-device-setup-done": HASSDomEvent<
|
||||
HASSDomEvents["improv-device-setup-done"]
|
||||
>;
|
||||
"matter-commission-finish": HASSDomEvent<
|
||||
HASSDomEvents["matter-commission-finish"]
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
+21
-15
@@ -3,6 +3,8 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, eventOptions, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { restoreScroll } from "../common/decorators/restore-scroll";
|
||||
import type { HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { goBack } from "../common/navigate";
|
||||
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
@@ -36,19 +38,14 @@ class HassSubpage extends LitElement {
|
||||
<div class="toolbar ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar-content">
|
||||
${
|
||||
this.mainPage || history.state?.root
|
||||
this.mainPage || (!backPath && history.state?.root)
|
||||
? html`<ha-menu-button></ha-menu-button>`
|
||||
: backPath
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
href=${backPath}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.href=${backPath}
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
}
|
||||
|
||||
<div class="main-title">
|
||||
@@ -74,16 +71,25 @@ class HassSubpage extends LitElement {
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _saveScrollPos(e: Event) {
|
||||
private _saveScrollPos(e: HASSDomTargetEvent<HTMLDivElement>) {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
private _backTapped(): void {
|
||||
private _backTapped(ev: MouseEvent): void {
|
||||
const backPath = sanitizeNavigationPath(this.backPath);
|
||||
|
||||
// Middle click, ctrl and cmd open the parent in a new tab, let the anchor
|
||||
// handle those.
|
||||
if (backPath && !isNavigationClick(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.backCallback) {
|
||||
this.backCallback();
|
||||
return;
|
||||
}
|
||||
goBack();
|
||||
|
||||
goBack(backPath);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent, type HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/chips/ha-assist-chip";
|
||||
import "../components/data-table/ha-data-table";
|
||||
@@ -740,7 +740,9 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
this._dataTable.clearSelection();
|
||||
};
|
||||
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
private _handleSearchChange(
|
||||
ev: InputEvent & HASSDomTargetEvent<HaInputSearch>
|
||||
) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
if (this.filter === target.value) {
|
||||
return;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { restoreScroll } from "../common/decorators/restore-scroll";
|
||||
import type { HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { goBack, navigate } from "../common/navigate";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
@@ -174,17 +175,12 @@ export class HassTabsSubpage extends LitElement {
|
||||
${
|
||||
this.mainPage || (!backPath && history.state?.root)
|
||||
? html`<ha-menu-button></ha-menu-button>`
|
||||
: backPath
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.href=${backPath}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.href=${backPath}
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
}
|
||||
${
|
||||
this._narrow || !this.showTabs
|
||||
@@ -232,7 +228,7 @@ export class HassTabsSubpage extends LitElement {
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _saveScrollPos(e: Event) {
|
||||
private _saveScrollPos(e: HASSDomTargetEvent<HTMLDivElement>) {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
@@ -245,12 +241,21 @@ export class HassTabsSubpage extends LitElement {
|
||||
this._content.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
private _backTapped(): void {
|
||||
private _backTapped(ev: MouseEvent): void {
|
||||
const backPath = sanitizeNavigationPath(this.backPath);
|
||||
|
||||
// Middle click, ctrl and cmd open the parent in a new tab, let the anchor
|
||||
// handle those.
|
||||
if (backPath && !isNavigationClick(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.backCallback) {
|
||||
this.backCallback();
|
||||
return;
|
||||
}
|
||||
goBack();
|
||||
|
||||
goBack(backPath);
|
||||
}
|
||||
|
||||
private _isActiveTabPath(tabPath: string, currentPath: string): boolean {
|
||||
|
||||
@@ -36,7 +36,7 @@ export const PreventUnsavedMixin = <T extends Constructor<LitElement>>(
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (this.isDirtyState) {
|
||||
if (this.isDirtyState && this.isConnected) {
|
||||
window.addEventListener("click", this._handleClick, true);
|
||||
window.addEventListener("beforeunload", this._handleUnload);
|
||||
} else {
|
||||
|
||||
@@ -37,7 +37,7 @@ import { subscribeUser } from "../data/ws-user";
|
||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { HassElement } from "../state/hass-element";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import "../components/progress/ha-progress-bar";
|
||||
@@ -80,8 +80,8 @@ declare global {
|
||||
}
|
||||
|
||||
interface GlobalEventHandlersEventMap {
|
||||
"onboarding-step": HASSDomEvent<OnboardingEvent>;
|
||||
"onboarding-progress": HASSDomEvent<OnboardingProgressEvent>;
|
||||
"onboarding-step": HASSDomEvent<HASSDomEvents["onboarding-step"]>;
|
||||
"onboarding-progress": HASSDomEvent<HASSDomEvents["onboarding-progress"]>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +332,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleProgress(ev: HASSDomEvent<OnboardingProgressEvent>) {
|
||||
private _handleProgress(
|
||||
ev: HASSDomEvent<HASSDomEvents["onboarding-progress"]>
|
||||
) {
|
||||
const stepSize = 100 / this._steps!.length;
|
||||
if (ev.detail.increase) {
|
||||
this._progress += ev.detail.increase * stepSize;
|
||||
@@ -345,7 +347,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleStepDone(ev: HASSDomEvent<OnboardingEvent>) {
|
||||
private async _handleStepDone(
|
||||
ev: HASSDomEvent<HASSDomEvents["onboarding-step"]>
|
||||
) {
|
||||
const stepResult = ev.detail;
|
||||
this._steps = this._steps!.map((step) =>
|
||||
step.step === stepResult.type ? { ...step, done: true } : step
|
||||
@@ -479,7 +483,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _languageChanged(ev: CustomEvent) {
|
||||
private _languageChanged(ev: ValueChangedEvent<string>) {
|
||||
const language = ev.detail.value;
|
||||
this.language = language;
|
||||
if (this.hass) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mdiOpenInNew } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent, type HASSDomEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-analytics";
|
||||
import "../components/ha-button";
|
||||
@@ -65,7 +65,9 @@ class OnboardingAnalytics extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _preferencesChanged(event: CustomEvent): void {
|
||||
private _preferencesChanged(
|
||||
event: HASSDomEvent<HASSDomEvents["analytics-preferences-changed"]>
|
||||
): void {
|
||||
this._analyticsDetails = {
|
||||
...this._analyticsDetails!,
|
||||
preferences: event.detail.preferences,
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { ConfigUpdateValues } from "../data/core";
|
||||
import { saveCoreConfig } from "../data/core";
|
||||
import { countryCurrency } from "../data/currency";
|
||||
import { onboardCoreConfigStep } from "../data/onboarding";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { getLocalLanguage } from "../util/common-translation";
|
||||
import "./onboarding-location";
|
||||
|
||||
@@ -132,7 +132,9 @@ class OnboardingCoreConfig extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleFormChanged(ev: CustomEvent) {
|
||||
private _handleFormChanged(
|
||||
ev: ValueChangedEvent<{ country?: string; time_zone?: string }>
|
||||
) {
|
||||
const value = ev.detail.value as { country?: string; time_zone?: string };
|
||||
this._country = value.country || undefined;
|
||||
if (value.time_zone) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { navigate } from "../common/navigate";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { removeSearchParam } from "../common/url/search-params";
|
||||
@@ -280,7 +281,9 @@ class OnboardingRestoreBackup extends LitElement {
|
||||
setTimeout(() => this._loadBackupInfo(), delay);
|
||||
}
|
||||
|
||||
private async _backupUploaded(ev: CustomEvent) {
|
||||
private async _backupUploaded(
|
||||
ev: HASSDomEvent<HASSDomEvents["backup-uploaded"]>
|
||||
) {
|
||||
this._backupId = ev.detail.backupId;
|
||||
await this._loadBackupInfo();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { formatDateTimeWithBrowserDefaults } from "../../common/datetime/format_date_time";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../../components/buttons/ha-progress-button";
|
||||
import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
|
||||
@@ -9,6 +13,7 @@ import "../../components/ha-alert";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/input/ha-input";
|
||||
import type { HaInput } from "../../components/input/ha-input";
|
||||
import "../../components/item/ha-row-item";
|
||||
import {
|
||||
getPreferredAgentForDownload,
|
||||
@@ -19,6 +24,7 @@ import { restoreOnboardingBackup } from "../../data/backup_onboarding";
|
||||
import "../../panels/config/backup/components/ha-backup-data-picker";
|
||||
import "../../panels/config/backup/components/ha-backup-formfield-label";
|
||||
import { onBoardingStyles } from "../styles";
|
||||
import type { ValueChangedEvent } from "../../types";
|
||||
|
||||
@customElement("onboarding-restore-backup-restore")
|
||||
class OnboardingRestoreBackupRestore extends LitElement {
|
||||
@@ -247,16 +253,20 @@ class OnboardingRestoreBackupRestore extends LitElement {
|
||||
fireEvent(this, "sign-out");
|
||||
}
|
||||
|
||||
private _selectedBackupChanged(ev: CustomEvent) {
|
||||
private _selectedBackupChanged(ev: ValueChangedEvent<BackupData>) {
|
||||
ev.stopPropagation();
|
||||
this._selectedData = ev.detail.value;
|
||||
}
|
||||
|
||||
private _encryptionKeyChanged(ev): void {
|
||||
private _encryptionKeyChanged(
|
||||
ev: HASSDomTargetEvent<Omit<HaInput, "value"> & { value: string }>
|
||||
): void {
|
||||
this._encryptionKey = ev.target.value;
|
||||
}
|
||||
|
||||
private async _startRestore(ev: CustomEvent): Promise<void> {
|
||||
private async _startRestore(
|
||||
ev: HASSDomCurrentTargetEvent<HaProgressButton>
|
||||
): Promise<void> {
|
||||
const agentId = Object.keys(this.backup.agents)[0];
|
||||
const backupProtected = this.backup.agents[agentId].protected;
|
||||
|
||||
@@ -270,7 +280,7 @@ class OnboardingRestoreBackupRestore extends LitElement {
|
||||
}
|
||||
|
||||
this._loading = true;
|
||||
const button = ev.currentTarget as HaProgressButton;
|
||||
const button = ev.currentTarget;
|
||||
this._error = undefined;
|
||||
this._encryptionKeyWrong = false;
|
||||
|
||||
|
||||
@@ -6,14 +6,20 @@ import type { ByWeekday, Options, WeekdayStr } from "rrule";
|
||||
import { RRule, Weekday } from "rrule";
|
||||
import { formatDate, formatTime } from "../../common/datetime/calc_date";
|
||||
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import "../../components/chips/ha-chip-set";
|
||||
import "../../components/chips/ha-filter-chip";
|
||||
import type { HaFilterChip } from "../../components/chips/ha-filter-chip";
|
||||
import "../../components/ha-date-input";
|
||||
import "../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../components/ha-select";
|
||||
import "../../components/input/ha-input";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HaInput } from "../../components/input/ha-input";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type {
|
||||
MonthlyRepeatItem,
|
||||
RepeatEnd,
|
||||
@@ -321,8 +327,8 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _onIntervalChange(e: Event) {
|
||||
this._interval = (e.target! as any).value;
|
||||
private _onIntervalChange(e: HASSDomTargetEvent<HaInput>) {
|
||||
this._interval = Number(e.target.value);
|
||||
}
|
||||
|
||||
private _onRepeatSelected(e: HaSelectSelectEvent<RepeatFrequency>) {
|
||||
@@ -351,9 +357,12 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
this._monthday = selectedItem.bymonthday;
|
||||
}
|
||||
|
||||
private _onWeekdayToggle(e: MouseEvent) {
|
||||
const target = e.currentTarget as any;
|
||||
const value = target.value as WeekdayStr;
|
||||
private _onWeekdayToggle(
|
||||
e: MouseEvent &
|
||||
HASSDomCurrentTargetEvent<HaFilterChip & { value: WeekdayStr }>
|
||||
) {
|
||||
const target = e.currentTarget;
|
||||
const value = target.value;
|
||||
if (this._weekday.has(value)) {
|
||||
this._weekday.delete(value);
|
||||
} else {
|
||||
@@ -385,11 +394,11 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
private _onCountChange(e: Event) {
|
||||
this._count = (e.target! as any).value;
|
||||
private _onCountChange(e: HASSDomTargetEvent<HaInput>) {
|
||||
this._count = Number(e.target.value);
|
||||
}
|
||||
|
||||
private _onUntilChange(e: CustomEvent) {
|
||||
private _onUntilChange(e: ValueChangedEvent<string>) {
|
||||
e.stopPropagation();
|
||||
this._untilDay = new Date(
|
||||
new TZDate(e.detail.value + "T00:00:00", this.timezone).getTime()
|
||||
@@ -436,7 +445,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
);
|
||||
// rrule.js can't compute some UNTIL variations so we compute that ourself. Must be
|
||||
// in the same format as dtstart.
|
||||
let newUntilValue;
|
||||
let newUntilValue: string;
|
||||
if (this.allDay) {
|
||||
// For all-day events, only use the date part
|
||||
newUntilValue = until.toISOString().split("T")[0].replace(/-/g, "");
|
||||
|
||||
@@ -644,6 +644,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/areas/dashboard"
|
||||
.header=${html`${
|
||||
area.icon
|
||||
? html`<ha-icon
|
||||
|
||||
@@ -86,8 +86,6 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
|
||||
@state() private _hierarchy?: AreasFloorHierarchy;
|
||||
|
||||
private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
private _blockHierarchyUpdate = false;
|
||||
|
||||
private _blockHierarchyUpdateTimeout?: number;
|
||||
@@ -168,9 +166,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.areas}
|
||||
.route=${this.route}
|
||||
has-fab
|
||||
|
||||
@@ -317,7 +317,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
color:
|
||||
automation.state === UNAVAILABLE
|
||||
? "var(--error-color)"
|
||||
: "unset",
|
||||
: automation.state === "on"
|
||||
? "var(--state-active-color)"
|
||||
: "unset",
|
||||
})}
|
||||
></ha-state-icon>`,
|
||||
},
|
||||
@@ -346,19 +348,17 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
maxWidth: "82px",
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
hidden: narrow,
|
||||
type: "overflow",
|
||||
title: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||
template: (automation) =>
|
||||
narrow
|
||||
? automation.formatted_state
|
||||
: html`
|
||||
<ha-switch
|
||||
@click=${stopPropagation}
|
||||
@change=${this._handleSwitchToggle}
|
||||
.automation=${automation}
|
||||
.checked=${automation.state === "on"}
|
||||
></ha-switch>
|
||||
`,
|
||||
template: (automation) => html`
|
||||
<ha-switch
|
||||
@click=${stopPropagation}
|
||||
@change=${this._handleSwitchToggle}
|
||||
.automation=${automation}
|
||||
.checked=${automation.state === "on"}
|
||||
></ha-switch>
|
||||
`,
|
||||
},
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
@@ -443,9 +443,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
back-path="/config"
|
||||
id="entity_id"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
@@ -110,6 +110,7 @@ export class HaAutomationTrace extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/automation/dashboard"
|
||||
.header=${title}
|
||||
.scrollable=${this.narrow}
|
||||
>
|
||||
@@ -452,11 +453,7 @@ export class HaAutomationTrace extends LitElement {
|
||||
if (runId) {
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.delete("run_id");
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${location.pathname}?${params.toString()}`
|
||||
);
|
||||
replaceCurrentUrl(`${location.pathname}?${params.toString()}`);
|
||||
}
|
||||
|
||||
await showAlertDialog(this, {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import {
|
||||
extractSearchParam,
|
||||
@@ -171,11 +172,7 @@ export const ManualEditorMixin = <TConfig>(
|
||||
}
|
||||
|
||||
protected clearParam(param: string) {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(removeSearchParam(param))
|
||||
);
|
||||
replaceCurrentUrl(constructUrlCurrentPath(removeSearchParam(param)));
|
||||
}
|
||||
|
||||
protected async openSidebar(ev: CustomEvent<SidebarConfig>) {
|
||||
|
||||
@@ -51,6 +51,8 @@ const baseConfigStruct = object({
|
||||
mode: optional(string()),
|
||||
max_exceeded: optional(string()),
|
||||
id: optional(string()),
|
||||
variables: optional(object()),
|
||||
trigger_variables: optional(object()),
|
||||
});
|
||||
|
||||
const automationConfigStruct = union([
|
||||
@@ -257,6 +259,27 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
|
||||
}
|
||||
}
|
||||
|
||||
// If an object can be ambiguously an action or an automation, check for
|
||||
// toplevel automation keywords to disqualify it
|
||||
function isQualifiedAction(cfg): boolean {
|
||||
const type = getActionType(cfg);
|
||||
if (type === "variables") {
|
||||
if (
|
||||
[
|
||||
"trigger",
|
||||
"triggers",
|
||||
"condition",
|
||||
"conditions",
|
||||
"action",
|
||||
"actions",
|
||||
].some((key) => key in cfg)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return type !== "unknown";
|
||||
}
|
||||
|
||||
if (Array.isArray(config)) {
|
||||
if (config.length === 1) {
|
||||
config = config[0];
|
||||
@@ -276,7 +299,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
|
||||
found = true;
|
||||
(newConfig.conditions as Condition[]).push(cfg);
|
||||
}
|
||||
if (getActionType(cfg) !== "unknown") {
|
||||
if (isQualifiedAction(cfg)) {
|
||||
found = true;
|
||||
(newConfig.actions as Action[]).push(cfg);
|
||||
}
|
||||
@@ -293,7 +316,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
|
||||
if (isCondition(config)) {
|
||||
config = { conditions: [config] };
|
||||
}
|
||||
if (getActionType(config) !== "unknown") {
|
||||
if (isQualifiedAction(config)) {
|
||||
config = { actions: [config] };
|
||||
}
|
||||
|
||||
@@ -375,6 +398,11 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
|
||||
if (!workingCopy) {
|
||||
return;
|
||||
}
|
||||
["variables", "trigger_variables"].forEach((key) => {
|
||||
if (key in config) {
|
||||
workingCopy[key] = { ...workingCopy[key], ...config[key] };
|
||||
}
|
||||
});
|
||||
|
||||
if ("triggers" in config) {
|
||||
workingCopy.triggers = ensureArray(workingCopy.triggers || []).concat(
|
||||
|
||||
@@ -74,8 +74,6 @@ class HaConfigBackupOverview extends LitElement {
|
||||
|
||||
@state() private _config?: BackupConfig;
|
||||
|
||||
private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("config") && !this._config) {
|
||||
@@ -206,9 +204,7 @@ class HaConfigBackupOverview extends LitElement {
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config/system"
|
||||
}
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.backup.overview.header")}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-alert";
|
||||
@@ -118,7 +119,7 @@ class HaConfigBackupSettings extends LitElement {
|
||||
}
|
||||
|
||||
private _clearHash() {
|
||||
history.replaceState(null, "", window.location.pathname);
|
||||
replaceCurrentUrl(window.location.pathname);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -21,6 +21,7 @@ export class CloudForgotPassword extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.cloud.forgot_password.title"
|
||||
)}
|
||||
|
||||
@@ -45,6 +45,7 @@ export class CloudLoginPanel extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
header="Home Assistant Cloud"
|
||||
>
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
|
||||
@@ -38,6 +38,7 @@ export class CloudRegister extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.header=${this.hass.localize("ui.panel.config.cloud.register.title")}
|
||||
>
|
||||
<div class="content">
|
||||
|
||||
@@ -66,8 +66,6 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _showSkipped = false;
|
||||
|
||||
@state() private _supervisorInfo?: HassioSupervisorInfo;
|
||||
@@ -155,9 +153,7 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config/system"
|
||||
}
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.updates.caption")}
|
||||
|
||||
@@ -986,6 +986,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
return html`<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/devices/dashboard"
|
||||
.header=${deviceName}
|
||||
>
|
||||
<ha-tooltip for="edit-settings-button" slot="toolbar-icon">
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { navigate, updateHistoryState } from "../../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
@@ -778,9 +778,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.devices}
|
||||
.route=${this.route}
|
||||
.searchLabel=${this.hass.localize(
|
||||
@@ -1043,7 +1041,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
history.replaceState({ filter: this._filter }, "");
|
||||
updateHistoryState({ filter: this._filter });
|
||||
}
|
||||
|
||||
private _addDevice() {
|
||||
|
||||
@@ -78,8 +78,6 @@ class HaConfigEnergy extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _info?: EnergyInfo;
|
||||
|
||||
@state() private _preferences?: EnergyPreferences;
|
||||
@@ -126,11 +124,7 @@ class HaConfigEnergy extends LitElement {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config/lovelace/dashboards"
|
||||
}
|
||||
back-path="/config/lovelace/dashboards"
|
||||
.route=${this.route}
|
||||
.tabs=${TABS}
|
||||
>
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { updateHistoryState } from "../../../common/navigate";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
@@ -810,9 +811,7 @@ export class HaConfigEntities extends LitElement {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(this.hass.localize, filteredEntities)}
|
||||
@@ -1246,7 +1245,7 @@ export class HaConfigEntities extends LitElement {
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
history.replaceState({ filter: this._filter }, "");
|
||||
updateHistoryState({ filter: this._filter });
|
||||
}
|
||||
|
||||
private _handleSelectionChanged(
|
||||
|
||||
@@ -644,9 +644,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.searchLabel=${this.hass.localize(
|
||||
|
||||
@@ -373,7 +373,11 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
: this._manifest?.documentation;
|
||||
|
||||
return html`
|
||||
<hass-subpage .hass=${this.hass} .narrow=${this.narrow}>
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/integrations/dashboard"
|
||||
>
|
||||
${
|
||||
documentationLink
|
||||
? html`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user