mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-23 08:27:36 +00:00
Compare commits
6 Commits
quick-bar-
...
fixed_ener
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6745a97f86 | ||
|
|
a8286e31a0 | ||
|
|
4fa7274fbe | ||
|
|
414c1315a4 | ||
|
|
04bfc350ea | ||
|
|
75ec9404ce |
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -154,7 +154,7 @@ try {
|
|||||||
|
|
||||||
- **Use CSS custom properties**: Leverage the theme system
|
- **Use CSS custom properties**: Leverage the theme system
|
||||||
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
||||||
- Spacing scale: `--ha-space-1` (4px) through `--ha-space-20` (80px) in 4px increments
|
- Spacing scale: `--ha-space-0` (0px) through `--ha-space-20` (80px) in 4px increments
|
||||||
- Defined in `src/resources/theme/core.globals.ts`
|
- Defined in `src/resources/theme/core.globals.ts`
|
||||||
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
|
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
|
||||||
- **Mobile-first responsive**: Design for mobile, enhance for desktop
|
- **Mobile-first responsive**: Design for mobile, enhance for desktop
|
||||||
|
|||||||
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Build resources
|
- name: Build resources
|
||||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||||
- name: Setup lint cache
|
- name: Setup lint cache
|
||||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
node_modules/.cache/prettier
|
node_modules/.cache/prettier
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: frontend-bundle-stats
|
name: frontend-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: supervisor-bundle-stats
|
name: supervisor-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
|
|||||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -36,14 +36,14 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -57,4 +57,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||||
|
|||||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
process-only: "issues, prs"
|
process-only: "issues, prs"
|
||||||
|
|||||||
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@@ -57,14 +57,14 @@ jobs:
|
|||||||
run: tar -czvf translations.tar.gz translations
|
run: tar -czvf translations.tar.gz translations
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: wheels
|
name: wheels
|
||||||
path: dist/home_assistant_frontend*.whl
|
path: dist/home_assistant_frontend*.whl
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -56,5 +56,4 @@ test/coverage/
|
|||||||
|
|
||||||
# AI tooling
|
# AI tooling
|
||||||
.claude
|
.claude
|
||||||
.cursor
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
compressionLevel: mixed
|
compressionLevel: mixed
|
||||||
|
|
||||||
npmMinimalAgeGate: "3d"
|
|
||||||
|
|
||||||
defaultSemverRangePrefix: ""
|
defaultSemverRangePrefix: ""
|
||||||
|
|
||||||
enableGlobalCache: false
|
enableGlobalCache: false
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ module.exports.ignorePackages = () => [];
|
|||||||
// Files from NPM packages that we should replace with empty file
|
// Files from NPM packages that we should replace with empty file
|
||||||
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
|
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
|
||||||
[
|
[
|
||||||
|
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
||||||
|
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
||||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||||
(isHassioBuild || isLandingPageBuild) &&
|
(isHassioBuild || isLandingPageBuild) &&
|
||||||
require.resolve(
|
require.resolve(
|
||||||
|
|||||||
@@ -168,16 +168,12 @@ const createRspackConfig = ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).length
|
new rspack.NormalModuleReplacementPlugin(
|
||||||
? new rspack.NormalModuleReplacementPlugin(
|
|
||||||
new RegExp(
|
new RegExp(
|
||||||
bundle
|
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
|
||||||
.emptyPackages({ isHassioBuild, isLandingPageBuild })
|
|
||||||
.join("|")
|
|
||||||
),
|
),
|
||||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||||
)
|
),
|
||||||
: false,
|
|
||||||
!isProdBuild && new LogStartCompilePlugin(),
|
!isProdBuild && new LogStartCompilePlugin(),
|
||||||
isProdBuild &&
|
isProdBuild &&
|
||||||
new StatsWriterPlugin({
|
new StatsWriterPlugin({
|
||||||
@@ -221,42 +217,6 @@ const createRspackConfig = ({
|
|||||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
||||||
"@lit-labs/observers/resize-controller":
|
"@lit-labs/observers/resize-controller":
|
||||||
"@lit-labs/observers/resize-controller.js",
|
"@lit-labs/observers/resize-controller.js",
|
||||||
"@formatjs/intl-durationformat/should-polyfill$":
|
|
||||||
"@formatjs/intl-durationformat/should-polyfill.js",
|
|
||||||
"@formatjs/intl-durationformat/polyfill-force$":
|
|
||||||
"@formatjs/intl-durationformat/polyfill-force.js",
|
|
||||||
"@formatjs/intl-datetimeformat/should-polyfill":
|
|
||||||
"@formatjs/intl-datetimeformat/should-polyfill.js",
|
|
||||||
"@formatjs/intl-datetimeformat/polyfill-force":
|
|
||||||
"@formatjs/intl-datetimeformat/polyfill-force.js",
|
|
||||||
"@formatjs/intl-displaynames/should-polyfill":
|
|
||||||
"@formatjs/intl-displaynames/should-polyfill.js",
|
|
||||||
"@formatjs/intl-displaynames/polyfill-force":
|
|
||||||
"@formatjs/intl-displaynames/polyfill-force.js",
|
|
||||||
"@formatjs/intl-getcanonicallocales/should-polyfill":
|
|
||||||
"@formatjs/intl-getcanonicallocales/should-polyfill.js",
|
|
||||||
"@formatjs/intl-getcanonicallocales/polyfill-force":
|
|
||||||
"@formatjs/intl-getcanonicallocales/polyfill-force.js",
|
|
||||||
"@formatjs/intl-listformat/should-polyfill":
|
|
||||||
"@formatjs/intl-listformat/should-polyfill.js",
|
|
||||||
"@formatjs/intl-listformat/polyfill-force":
|
|
||||||
"@formatjs/intl-listformat/polyfill-force.js",
|
|
||||||
"@formatjs/intl-locale/should-polyfill":
|
|
||||||
"@formatjs/intl-locale/should-polyfill.js",
|
|
||||||
"@formatjs/intl-locale/polyfill-force":
|
|
||||||
"@formatjs/intl-locale/polyfill-force.js",
|
|
||||||
"@formatjs/intl-numberformat/should-polyfill":
|
|
||||||
"@formatjs/intl-numberformat/should-polyfill.js",
|
|
||||||
"@formatjs/intl-numberformat/polyfill-force":
|
|
||||||
"@formatjs/intl-numberformat/polyfill-force.js",
|
|
||||||
"@formatjs/intl-pluralrules/should-polyfill":
|
|
||||||
"@formatjs/intl-pluralrules/should-polyfill.js",
|
|
||||||
"@formatjs/intl-pluralrules/polyfill-force":
|
|
||||||
"@formatjs/intl-pluralrules/polyfill-force.js",
|
|
||||||
"@formatjs/intl-relativetimeformat/should-polyfill":
|
|
||||||
"@formatjs/intl-relativetimeformat/should-polyfill.js",
|
|
||||||
"@formatjs/intl-relativetimeformat/polyfill-force":
|
|
||||||
"@formatjs/intl-relativetimeformat/polyfill-force.js",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
@@ -5,19 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance();
|
|||||||
const playerManager = castContext.getPlayerManager();
|
const playerManager = castContext.getPlayerManager();
|
||||||
|
|
||||||
playerManager.setMessageInterceptor(
|
playerManager.setMessageInterceptor(
|
||||||
"LOAD" as framework.messages.MessageType.LOAD,
|
framework.messages.MessageType.LOAD,
|
||||||
(loadRequestData) => {
|
(loadRequestData) => {
|
||||||
const media = loadRequestData.media;
|
const media = loadRequestData.media;
|
||||||
// Special handling if it came from Google Assistant
|
// Special handling if it came from Google Assistant
|
||||||
if (media.entity) {
|
if (media.entity) {
|
||||||
media.contentId = media.entity;
|
media.contentId = media.entity;
|
||||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
media.streamType = framework.messages.StreamType.LIVE;
|
||||||
media.contentType = "application/vnd.apple.mpegurl";
|
media.contentType = "application/vnd.apple.mpegurl";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
|
||||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
|
||||||
media.hlsVideoSegmentFormat =
|
media.hlsVideoSegmentFormat =
|
||||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||||
}
|
}
|
||||||
return loadRequestData;
|
return loadRequestData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { framework } from "./cast_framework";
|
||||||
import { CAST_NS } from "../../../src/cast/const";
|
import { CAST_NS } from "../../../src/cast/const";
|
||||||
import type { HassMessage } from "../../../src/cast/receiver_messages";
|
import type { HassMessage } from "../../../src/cast/receiver_messages";
|
||||||
import "../../../src/resources/custom-card-support";
|
import "../../../src/resources/custom-card-support";
|
||||||
import { castContext } from "./cast_context";
|
import { castContext } from "./cast_context";
|
||||||
import { framework } from "./cast_framework";
|
|
||||||
import { HcMain } from "./layout/hc-main";
|
import { HcMain } from "./layout/hc-main";
|
||||||
|
import type { ReceivedMessage } from "./types";
|
||||||
|
|
||||||
const lovelaceController = new HcMain();
|
const lovelaceController = new HcMain();
|
||||||
document.body.append(lovelaceController);
|
document.body.append(lovelaceController);
|
||||||
@@ -39,8 +40,7 @@ const playDummyMedia = (viewTitle?: string) => {
|
|||||||
loadRequestData.media.contentId =
|
loadRequestData.media.contentId =
|
||||||
"https://cast.home-assistant.io/images/google-nest-hub.png";
|
"https://cast.home-assistant.io/images/google-nest-hub.png";
|
||||||
loadRequestData.media.contentType = "image/jpeg";
|
loadRequestData.media.contentType = "image/jpeg";
|
||||||
loadRequestData.media.streamType =
|
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
|
||||||
"NONE" as framework.messages.StreamType.NONE;
|
|
||||||
const metadata = new framework.messages.GenericMediaMetadata();
|
const metadata = new framework.messages.GenericMediaMetadata();
|
||||||
metadata.title = viewTitle;
|
metadata.title = viewTitle;
|
||||||
loadRequestData.media.metadata = metadata;
|
loadRequestData.media.metadata = metadata;
|
||||||
@@ -89,30 +89,31 @@ const showMediaPlayer = () => {
|
|||||||
const options = new framework.CastReceiverOptions();
|
const options = new framework.CastReceiverOptions();
|
||||||
options.disableIdleTimeout = true;
|
options.disableIdleTimeout = true;
|
||||||
options.customNamespaces = {
|
options.customNamespaces = {
|
||||||
// type definition is wrong, should be "JSON" instead of "json"
|
[CAST_NS]: framework.system.MessageType.JSON,
|
||||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.system#.MessageType
|
|
||||||
[CAST_NS]: "JSON" as framework.system.MessageType.JSON,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
castContext.addCustomMessageListener(CAST_NS, (ev) => {
|
castContext.addCustomMessageListener(
|
||||||
|
CAST_NS,
|
||||||
|
// @ts-ignore
|
||||||
|
(ev: ReceivedMessage<HassMessage>) => {
|
||||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||||
if (
|
if (
|
||||||
playerManager.getPlayerState() !==
|
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
|
||||||
("IDLE" as framework.messages.PlayerState.IDLE)
|
|
||||||
) {
|
) {
|
||||||
playerManager.stop();
|
playerManager.stop();
|
||||||
} else {
|
} else {
|
||||||
showLovelaceController();
|
showLovelaceController();
|
||||||
}
|
}
|
||||||
const msg = ev.data as HassMessage;
|
const msg = ev.data;
|
||||||
msg.senderId = ev.senderId;
|
msg.senderId = ev.senderId;
|
||||||
lovelaceController.processIncomingMessage(msg);
|
lovelaceController.processIncomingMessage(msg);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const playerManager = castContext.getPlayerManager();
|
const playerManager = castContext.getPlayerManager();
|
||||||
|
|
||||||
playerManager.setMessageInterceptor(
|
playerManager.setMessageInterceptor(
|
||||||
"LOAD" as framework.messages.MessageType.LOAD,
|
framework.messages.MessageType.LOAD,
|
||||||
(loadRequestData) => {
|
(loadRequestData) => {
|
||||||
if (
|
if (
|
||||||
loadRequestData.media.contentId ===
|
loadRequestData.media.contentId ===
|
||||||
@@ -126,26 +127,24 @@ playerManager.setMessageInterceptor(
|
|||||||
// Special handling if it came from Google Assistant
|
// Special handling if it came from Google Assistant
|
||||||
if (media.entity) {
|
if (media.entity) {
|
||||||
media.contentId = media.entity;
|
media.contentId = media.entity;
|
||||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
media.streamType = framework.messages.StreamType.LIVE;
|
||||||
media.contentType = "application/vnd.apple.mpegurl";
|
media.contentType = "application/vnd.apple.mpegurl";
|
||||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
// @ts-ignore
|
||||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
|
||||||
media.hlsVideoSegmentFormat =
|
media.hlsVideoSegmentFormat =
|
||||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||||
}
|
}
|
||||||
return loadRequestData;
|
return loadRequestData;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
playerManager.addEventListener(
|
playerManager.addEventListener(
|
||||||
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
|
framework.events.EventType.MEDIA_STATUS,
|
||||||
(event) => {
|
(event) => {
|
||||||
if (
|
if (
|
||||||
event.mediaStatus?.playerState ===
|
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
|
||||||
("IDLE" as framework.messages.PlayerState.IDLE) &&
|
|
||||||
event.mediaStatus?.idleReason &&
|
event.mediaStatus?.idleReason &&
|
||||||
event.mediaStatus?.idleReason !==
|
event.mediaStatus?.idleReason !==
|
||||||
("INTERRUPTED" as framework.messages.IdleReason.INTERRUPTED)
|
framework.messages.IdleReason.INTERRUPTED
|
||||||
) {
|
) {
|
||||||
// media finished or stopped, return to default Lovelace
|
// media finished or stopped, return to default Lovelace
|
||||||
showLovelaceController();
|
showLovelaceController();
|
||||||
|
|||||||
6
cast/src/receiver/types.ts
Normal file
6
cast/src/receiver/types.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface ReceivedMessage<T> {
|
||||||
|
gj: boolean;
|
||||||
|
data: T;
|
||||||
|
senderId: string;
|
||||||
|
type: "message";
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DeviceRegistryEntry } from "../../../src/data/device/device_registry";
|
import type { DeviceRegistryEntry } from "../../../src/data/device_registry";
|
||||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
export const mockDeviceRegistry = (
|
export const mockDeviceRegistry = (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
|
import type { EntityRegistryEntry } from "../../../src/data/entity_registry";
|
||||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
export const mockEntityRegistry = (
|
export const mockEntityRegistry = (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { LabelRegistryEntry } from "../../../src/data/label/label_registry";
|
import type { LabelRegistryEntry } from "../../../src/data/label_registry";
|
||||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
export const mockLabelRegistry = (
|
export const mockLabelRegistry = (
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
|||||||
<div class="action">
|
<div class="action">
|
||||||
<span>
|
<span>
|
||||||
${this._action
|
${this._action
|
||||||
? describeAction(this.hass, [], this._action)
|
? describeAction(this.hass, [], [], {}, this._action)
|
||||||
: "<invalid YAML>"}
|
: "<invalid YAML>"}
|
||||||
</span>
|
</span>
|
||||||
<ha-yaml-editor
|
<ha-yaml-editor
|
||||||
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
|||||||
${ACTIONS.map(
|
${ACTIONS.map(
|
||||||
(conf) => html`
|
(conf) => html`
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<span>${describeAction(this.hass, [], conf as any)}</span>
|
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
|
||||||
<pre>${dump(conf)}</pre>
|
<pre>${dump(conf)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import { computeInitialHaFormData } from "../../../../src/components/ha-form/com
|
|||||||
import "../../../../src/components/ha-form/ha-form";
|
import "../../../../src/components/ha-form/ha-form";
|
||||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
|
||||||
import { getEntity } from "../../../../src/fake_data/entity";
|
import { getEntity } from "../../../../src/fake_data/entity";
|
||||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
import "../../components/demo-black-white-row";
|
import "../../components/demo-black-white-row";
|
||||||
|
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
||||||
|
|
||||||
const ENTITIES = [
|
const ENTITIES = [
|
||||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import "../../../../src/components/ha-selector/ha-selector";
|
|||||||
import "../../../../src/components/ha-settings-row";
|
import "../../../../src/components/ha-settings-row";
|
||||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
||||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||||
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
|
import type { LabelRegistryEntry } from "../../../../src/data/label_registry";
|
||||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||||
import { getEntity } from "../../../../src/fake_data/entity";
|
import { getEntity } from "../../../../src/fake_data/entity";
|
||||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||||
@@ -40,9 +40,6 @@ const ENTITIES = [
|
|||||||
getEntity("switch", "coffee", "off", {
|
getEntity("switch", "coffee", "off", {
|
||||||
friendly_name: "Coffee",
|
friendly_name: "Coffee",
|
||||||
}),
|
}),
|
||||||
getEntity("number", "number", 5, {
|
|
||||||
friendly_name: "Number",
|
|
||||||
}),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const DEVICES: DeviceRegistryEntry[] = [
|
const DEVICES: DeviceRegistryEntry[] = [
|
||||||
@@ -380,33 +377,6 @@ const SCHEMAS: {
|
|||||||
name: "Constant",
|
name: "Constant",
|
||||||
selector: { constant: { value: true, label: "Yes!" } },
|
selector: { constant: { value: true, label: "Yes!" } },
|
||||||
},
|
},
|
||||||
choose: {
|
|
||||||
name: "Choose",
|
|
||||||
selector: {
|
|
||||||
choose: {
|
|
||||||
choices: {
|
|
||||||
number: {
|
|
||||||
selector: {
|
|
||||||
number: {
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
entity: {
|
|
||||||
selector: {
|
|
||||||
entity: {
|
|
||||||
filter: {
|
|
||||||
domain: "number",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import type { IntegrationManifest } from "../../../../src/data/integration";
|
import type { IntegrationManifest } from "../../../../src/data/integration";
|
||||||
|
|
||||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
||||||
import type { EntityRegistryEntry } from "../../../../src/data/entity/entity_registry";
|
import type { EntityRegistryEntry } from "../../../../src/data/entity_registry";
|
||||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||||
import "../../../../src/panels/config/integrations/ha-config-flow-card";
|
import "../../../../src/panels/config/integrations/ha-config-flow-card";
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
56
package.json
56
package.json
@@ -29,30 +29,30 @@
|
|||||||
"@babel/runtime": "7.28.4",
|
"@babel/runtime": "7.28.4",
|
||||||
"@braintree/sanitize-url": "7.1.1",
|
"@braintree/sanitize-url": "7.1.1",
|
||||||
"@codemirror/autocomplete": "6.20.0",
|
"@codemirror/autocomplete": "6.20.0",
|
||||||
"@codemirror/commands": "6.10.1",
|
"@codemirror/commands": "6.10.0",
|
||||||
"@codemirror/language": "6.11.3",
|
"@codemirror/language": "6.11.3",
|
||||||
"@codemirror/legacy-modes": "6.5.2",
|
"@codemirror/legacy-modes": "6.5.2",
|
||||||
"@codemirror/search": "6.5.11",
|
"@codemirror/search": "6.5.11",
|
||||||
"@codemirror/state": "6.5.2",
|
"@codemirror/state": "6.5.2",
|
||||||
"@codemirror/view": "6.39.4",
|
"@codemirror/view": "6.38.8",
|
||||||
"@date-fns/tz": "1.4.1",
|
"@date-fns/tz": "1.4.1",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "7.0.2",
|
"@formatjs/intl-datetimeformat": "6.18.2",
|
||||||
"@formatjs/intl-displaynames": "7.0.2",
|
"@formatjs/intl-displaynames": "6.8.13",
|
||||||
"@formatjs/intl-durationformat": "0.8.2",
|
"@formatjs/intl-durationformat": "0.7.6",
|
||||||
"@formatjs/intl-getcanonicallocales": "3.0.2",
|
"@formatjs/intl-getcanonicallocales": "2.5.6",
|
||||||
"@formatjs/intl-listformat": "8.0.2",
|
"@formatjs/intl-listformat": "7.7.13",
|
||||||
"@formatjs/intl-locale": "5.0.2",
|
"@formatjs/intl-locale": "4.2.13",
|
||||||
"@formatjs/intl-numberformat": "9.0.3",
|
"@formatjs/intl-numberformat": "8.15.6",
|
||||||
"@formatjs/intl-pluralrules": "6.0.2",
|
"@formatjs/intl-pluralrules": "5.4.6",
|
||||||
"@formatjs/intl-relativetimeformat": "12.0.3",
|
"@formatjs/intl-relativetimeformat": "11.4.13",
|
||||||
"@fullcalendar/core": "6.1.19",
|
"@fullcalendar/core": "6.1.19",
|
||||||
"@fullcalendar/daygrid": "6.1.19",
|
"@fullcalendar/daygrid": "6.1.19",
|
||||||
"@fullcalendar/interaction": "6.1.19",
|
"@fullcalendar/interaction": "6.1.19",
|
||||||
"@fullcalendar/list": "6.1.19",
|
"@fullcalendar/list": "6.1.19",
|
||||||
"@fullcalendar/luxon3": "6.1.19",
|
"@fullcalendar/luxon3": "6.1.19",
|
||||||
"@fullcalendar/timegrid": "6.1.19",
|
"@fullcalendar/timegrid": "6.1.19",
|
||||||
"@home-assistant/webawesome": "3.0.0-ha.2",
|
"@home-assistant/webawesome": "3.0.0-ha.1",
|
||||||
"@lezer/highlight": "1.2.3",
|
"@lezer/highlight": "1.2.3",
|
||||||
"@lit-labs/motion": "1.0.9",
|
"@lit-labs/motion": "1.0.9",
|
||||||
"@lit-labs/observers": "2.0.6",
|
"@lit-labs/observers": "2.0.6",
|
||||||
@@ -89,6 +89,8 @@
|
|||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@tsparticles/engine": "3.9.1",
|
"@tsparticles/engine": "3.9.1",
|
||||||
"@tsparticles/preset-links": "3.2.0",
|
"@tsparticles/preset-links": "3.2.0",
|
||||||
|
"@vaadin/combo-box": "24.9.6",
|
||||||
|
"@vaadin/vaadin-themable-mixin": "24.9.6",
|
||||||
"@vibrant/color": "4.0.0",
|
"@vibrant/color": "4.0.0",
|
||||||
"@vue/web-component-wrapper": "1.3.0",
|
"@vue/web-component-wrapper": "1.3.0",
|
||||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||||
@@ -112,7 +114,7 @@
|
|||||||
"hls.js": "1.6.15",
|
"hls.js": "1.6.15",
|
||||||
"home-assistant-js-websocket": "9.6.0",
|
"home-assistant-js-websocket": "9.6.0",
|
||||||
"idb-keyval": "6.2.2",
|
"idb-keyval": "6.2.2",
|
||||||
"intl-messageformat": "11.0.2",
|
"intl-messageformat": "10.7.18",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||||
@@ -133,7 +135,7 @@
|
|||||||
"stacktrace-js": "2.0.2",
|
"stacktrace-js": "2.0.2",
|
||||||
"superstruct": "2.0.2",
|
"superstruct": "2.0.2",
|
||||||
"tinykeys": "3.0.0",
|
"tinykeys": "3.0.0",
|
||||||
"ua-parser-js": "2.0.7",
|
"ua-parser-js": "2.0.6",
|
||||||
"vue": "2.7.16",
|
"vue": "2.7.16",
|
||||||
"vue2-daterange-picker": "0.6.8",
|
"vue2-daterange-picker": "0.6.8",
|
||||||
"weekstart": "2.0.0",
|
"weekstart": "2.0.0",
|
||||||
@@ -151,15 +153,15 @@
|
|||||||
"@babel/plugin-transform-runtime": "7.28.5",
|
"@babel/plugin-transform-runtime": "7.28.5",
|
||||||
"@babel/preset-env": "7.28.5",
|
"@babel/preset-env": "7.28.5",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.21.7",
|
"@bundle-stats/plugin-webpack-filter": "4.21.7",
|
||||||
"@lokalise/node-api": "15.5.0",
|
"@lokalise/node-api": "15.4.0",
|
||||||
"@octokit/auth-oauth-device": "8.0.3",
|
"@octokit/auth-oauth-device": "8.0.3",
|
||||||
"@octokit/plugin-retry": "8.0.3",
|
"@octokit/plugin-retry": "8.0.3",
|
||||||
"@octokit/rest": "22.0.1",
|
"@octokit/rest": "22.0.1",
|
||||||
"@rsdoctor/rspack-plugin": "1.3.16",
|
"@rsdoctor/rspack-plugin": "1.3.12",
|
||||||
"@rspack/core": "1.6.8",
|
"@rspack/core": "1.6.6",
|
||||||
"@rspack/dev-server": "1.1.4",
|
"@rspack/dev-server": "1.1.4",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
"@types/chromecast-caf-receiver": "6.0.25",
|
"@types/chromecast-caf-receiver": "6.0.22",
|
||||||
"@types/chromecast-caf-sender": "1.0.11",
|
"@types/chromecast-caf-sender": "1.0.11",
|
||||||
"@types/color-name": "2.0.0",
|
"@types/color-name": "2.0.0",
|
||||||
"@types/culori": "4.0.1",
|
"@types/culori": "4.0.1",
|
||||||
@@ -176,12 +178,12 @@
|
|||||||
"@types/tar": "6.1.13",
|
"@types/tar": "6.1.13",
|
||||||
"@types/ua-parser-js": "0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@vitest/coverage-v8": "4.0.16",
|
"@vitest/coverage-v8": "4.0.15",
|
||||||
"babel-loader": "10.0.0",
|
"babel-loader": "10.0.0",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
"del": "8.0.1",
|
"del": "8.0.1",
|
||||||
"eslint": "9.39.2",
|
"eslint": "9.39.1",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-import-resolver-webpack": "0.13.10",
|
"eslint-import-resolver-webpack": "0.13.10",
|
||||||
@@ -191,7 +193,7 @@
|
|||||||
"eslint-plugin-unused-imports": "4.3.0",
|
"eslint-plugin-unused-imports": "4.3.0",
|
||||||
"eslint-plugin-wc": "3.0.2",
|
"eslint-plugin-wc": "3.0.2",
|
||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
"fs-extra": "11.3.3",
|
"fs-extra": "11.3.2",
|
||||||
"glob": "13.0.0",
|
"glob": "13.0.0",
|
||||||
"gulp": "5.0.1",
|
"gulp": "5.0.1",
|
||||||
"gulp-brotli": "3.0.0",
|
"gulp-brotli": "3.0.0",
|
||||||
@@ -199,7 +201,7 @@
|
|||||||
"gulp-rename": "2.1.0",
|
"gulp-rename": "2.1.0",
|
||||||
"html-minifier-terser": "7.2.0",
|
"html-minifier-terser": "7.2.0",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"jsdom": "27.3.0",
|
"jsdom": "27.2.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lint-staged": "16.2.7",
|
"lint-staged": "16.2.7",
|
||||||
"lit-analyzer": "2.0.3",
|
"lit-analyzer": "2.0.3",
|
||||||
@@ -212,12 +214,12 @@
|
|||||||
"serve": "14.2.5",
|
"serve": "14.2.5",
|
||||||
"sinon": "21.0.0",
|
"sinon": "21.0.0",
|
||||||
"tar": "7.5.2",
|
"tar": "7.5.2",
|
||||||
"terser-webpack-plugin": "5.3.16",
|
"terser-webpack-plugin": "5.3.15",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.50.0",
|
"typescript-eslint": "8.48.1",
|
||||||
"vite-tsconfig-paths": "6.0.3",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "4.0.16",
|
"vitest": "4.0.15",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
"webpackbar": "7.0.0",
|
"webpackbar": "7.0.0",
|
||||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||||
@@ -236,6 +238,6 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "yarn@4.12.0",
|
"packageManager": "yarn@4.12.0",
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "24.12.0"
|
"node": "22.21.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
DOMAIN_ATTRIBUTES_FORMATERS,
|
DOMAIN_ATTRIBUTES_FORMATERS,
|
||||||
DOMAIN_ATTRIBUTES_UNITS,
|
DOMAIN_ATTRIBUTES_UNITS,
|
||||||
TEMPERATURE_ATTRIBUTES,
|
TEMPERATURE_ATTRIBUTES,
|
||||||
} from "../../data/entity/entity_attributes";
|
} from "../../data/entity_attributes";
|
||||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||||
import type { FrontendLocaleData } from "../../data/translation";
|
import type { FrontendLocaleData } from "../../data/translation";
|
||||||
import type { WeatherEntity } from "../../data/weather";
|
import type { WeatherEntity } from "../../data/weather";
|
||||||
import { getWeatherUnit } from "../../data/weather";
|
import { getWeatherUnit } from "../../data/weather";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||||
import type {
|
import type {
|
||||||
EntityRegistryDisplayEntry,
|
EntityRegistryDisplayEntry,
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
} from "../../data/entity/entity_registry";
|
} from "../../data/entity_registry";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { getDuplicates } from "../string/get_duplicates";
|
|
||||||
import { computeStateName } from "./compute_state_name";
|
import { computeStateName } from "./compute_state_name";
|
||||||
|
import { getDuplicates } from "../string/get_duplicates";
|
||||||
|
|
||||||
export const computeDeviceName = (
|
export const computeDeviceName = (
|
||||||
device: DeviceRegistryEntry
|
device: DeviceRegistryEntry
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
|||||||
import type {
|
import type {
|
||||||
EntityRegistryDisplayEntry,
|
EntityRegistryDisplayEntry,
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
} from "../../data/entity/entity_registry";
|
} from "../../data/entity_registry";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { computeDeviceName } from "./compute_device_name";
|
import { computeDeviceName } from "./compute_device_name";
|
||||||
import { computeStateName } from "./compute_state_name";
|
import { computeStateName } from "./compute_state_name";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||||
import type { FrontendLocaleData } from "../../data/translation";
|
import type { FrontendLocaleData } from "../../data/translation";
|
||||||
import { TimeZone } from "../../data/translation";
|
import { TimeZone } from "../../data/translation";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
|
||||||
import { formatDate } from "../datetime/format_date";
|
import { formatDate } from "../datetime/format_date";
|
||||||
import { formatDateTime } from "../datetime/format_date_time";
|
import { formatDateTime } from "../datetime/format_date_time";
|
||||||
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
|
|
||||||
import { formatTime } from "../datetime/format_time";
|
import { formatTime } from "../datetime/format_time";
|
||||||
import {
|
import {
|
||||||
formatNumber,
|
formatNumber,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||||
import type {
|
import type {
|
||||||
EntityRegistryDisplayEntry,
|
EntityRegistryDisplayEntry,
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
ExtEntityRegistryEntry,
|
ExtEntityRegistryEntry,
|
||||||
} from "../../../data/entity/entity_registry";
|
} from "../../../data/entity_registry";
|
||||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { ConfigEntry } from "../../data/config_entries";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { deleteConfigEntry } from "../../data/config_entries";
|
|
||||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
|
||||||
import { removeEntityRegistryEntry } from "../../data/entity/entity_registry";
|
|
||||||
import { HELPERS_CRUD } from "../../data/helpers_crud";
|
|
||||||
import type { IntegrationManifest } from "../../data/integration";
|
import type { IntegrationManifest } from "../../data/integration";
|
||||||
|
import { computeDomain } from "./compute_domain";
|
||||||
|
import { HELPERS_CRUD } from "../../data/helpers_crud";
|
||||||
import type { Helper } from "../../panels/config/helpers/const";
|
import type { Helper } from "../../panels/config/helpers/const";
|
||||||
import { isHelperDomain } from "../../panels/config/helpers/const";
|
import { isHelperDomain } from "../../panels/config/helpers/const";
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import { isComponentLoaded } from "../config/is_component_loaded";
|
import { isComponentLoaded } from "../config/is_component_loaded";
|
||||||
import { computeDomain } from "./compute_domain";
|
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
||||||
|
import { removeEntityRegistryEntry } from "../../data/entity_registry";
|
||||||
|
import type { ConfigEntry } from "../../data/config_entries";
|
||||||
|
import { deleteConfigEntry } from "../../data/config_entries";
|
||||||
|
|
||||||
export const isDeletableEntity = (
|
export const isDeletableEntity = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { UNAVAILABLE_STATES } from "../../data/entity/entity";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import { stringCompare } from "../string/compare";
|
|
||||||
import { computeDomain } from "./compute_domain";
|
|
||||||
import { computeStateDomain } from "./compute_state_domain";
|
import { computeStateDomain } from "./compute_state_domain";
|
||||||
|
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { computeDomain } from "./compute_domain";
|
||||||
|
import { stringCompare } from "../string/compare";
|
||||||
|
|
||||||
export const FIXED_DOMAIN_STATES = {
|
export const FIXED_DOMAIN_STATES = {
|
||||||
alarm_control_panel: [
|
alarm_control_panel: [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import { computeStateDomain } from "./compute_state_domain";
|
import { computeStateDomain } from "./compute_state_domain";
|
||||||
|
import { isUnavailableState, UNAVAILABLE } from "../../data/entity";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
||||||
if (!states.length) {
|
if (!states.length) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity/entity";
|
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity";
|
||||||
import { computeDomain } from "./compute_domain";
|
import { computeDomain } from "./compute_domain";
|
||||||
|
|
||||||
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { UNAVAILABLE } from "../../data/entity/entity";
|
import { UNAVAILABLE } from "../../data/entity";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { DOMAINS_WITH_CARD } from "../const";
|
import { DOMAINS_WITH_CARD } from "../const";
|
||||||
import { canToggleState } from "./can_toggle_state";
|
import { canToggleState } from "./can_toggle_state";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** Return a color representing a state. */
|
/** Return a color representing a state. */
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { UNAVAILABLE } from "../../data/entity/entity";
|
import { UNAVAILABLE } from "../../data/entity";
|
||||||
import type { GroupEntity } from "../../data/group";
|
import type { GroupEntity } from "../../data/group";
|
||||||
import { computeGroupDomain } from "../../data/group";
|
import { computeGroupDomain } from "../../data/group";
|
||||||
import { computeCssVariable } from "../../resources/css-variables";
|
import { computeCssVariable } from "../../resources/css-variables";
|
||||||
|
|||||||
@@ -17,36 +17,25 @@ export interface NavigateOptions {
|
|||||||
// max time to wait for dialogs to close before navigating
|
// max time to wait for dialogs to close before navigating
|
||||||
const DIALOG_WAIT_TIMEOUT = 500;
|
const DIALOG_WAIT_TIMEOUT = 500;
|
||||||
|
|
||||||
/**
|
export const navigate = async (
|
||||||
* Ensures all dialogs are closed before navigation.
|
path: string,
|
||||||
* Returns true if navigation can proceed, false if a dialog refused to close.
|
options?: NavigateOptions,
|
||||||
*/
|
timestamp = Date.now()
|
||||||
const ensureDialogsClosed = async (timestamp: number): Promise<boolean> => {
|
) => {
|
||||||
const { history } = mainWindow;
|
const { history } = mainWindow;
|
||||||
|
if (history.state?.dialog && Date.now() - timestamp < DIALOG_WAIT_TIMEOUT) {
|
||||||
if (!history.state?.dialog || Date.now() - timestamp >= DIALOG_WAIT_TIMEOUT) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const closed = await closeAllDialogs();
|
const closed = await closeAllDialogs();
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn("Navigation blocked, because dialog refused to close");
|
console.warn("Navigation blocked, because dialog refused to close");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
// wait for history state to be updated after dialog closed
|
// need to wait for history state to be updated in case a dialog was closed
|
||||||
await new Promise<void>((resolve) => {
|
setTimeout(() => {
|
||||||
setTimeout(resolve);
|
navigate(path, options, timestamp).then(resolve);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return ensureDialogsClosed(timestamp);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const navigate = async (path: string, options?: NavigateOptions) => {
|
|
||||||
const canProceed = await ensureDialogsClosed(Date.now());
|
|
||||||
if (!canProceed) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
const replace = options?.replace || false;
|
const replace = options?.replace || false;
|
||||||
|
|
||||||
@@ -79,14 +68,10 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
|
|||||||
* Navigate back in history, with fallback to a default path if no history exists.
|
* 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.
|
* This prevents a user from getting stuck when they navigate directly to a page with no history.
|
||||||
*/
|
*/
|
||||||
export const goBack = async (fallbackPath?: string): Promise<void> => {
|
export const goBack = (fallbackPath?: string) => {
|
||||||
const canProceed = await ensureDialogsClosed(Date.now());
|
const { history } = mainWindow;
|
||||||
if (!canProceed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have history to go back to
|
// Check if we have history to go back to
|
||||||
const { history } = mainWindow;
|
|
||||||
if (history.length > 1) {
|
if (history.length > 1) {
|
||||||
history.back();
|
history.back();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
HassEntity,
|
HassEntity,
|
||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||||
import type { FrontendLocaleData } from "../../data/translation";
|
import type { FrontendLocaleData } from "../../data/translation";
|
||||||
import { NumberFormat } from "../../data/translation";
|
import { NumberFormat } from "../../data/translation";
|
||||||
import { round } from "./round";
|
import { round } from "./round";
|
||||||
|
|||||||
@@ -1,45 +1,30 @@
|
|||||||
/**
|
/**
|
||||||
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
|
* Executes a callback within a View Transition if supported, otherwise runs it directly.
|
||||||
*
|
*
|
||||||
* @param callback - Synchronous function to execute. The callback will be passed a boolean indicating whether the view transition is available.
|
* @param callback - Function to execute. Can be synchronous or return a Promise. The callback will be passed a boolean indicating whether the view transition is available.
|
||||||
* @returns Promise that resolves when the transition completes (or immediately if not supported)
|
* @returns Promise that resolves when the transition completes (or immediately if not supported)
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
|
* // Synchronous callback
|
||||||
* withViewTransition(() => {
|
* withViewTransition(() => {
|
||||||
* this.large = !this.large;
|
* this.large = !this.large;
|
||||||
* });
|
* });
|
||||||
|
*
|
||||||
|
* // Async callback
|
||||||
|
* await withViewTransition(async () => {
|
||||||
|
* await this.updateData();
|
||||||
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const withViewTransition = (
|
export const withViewTransition = (
|
||||||
callback: (viewTransitionAvailable: boolean) => void
|
callback: (viewTransitionAvailable: boolean) => void | Promise<void>
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (!document.startViewTransition) {
|
if (document.startViewTransition) {
|
||||||
callback(false);
|
return document.startViewTransition(() => callback(true)).finished;
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let callbackInvoked = false;
|
// Fallback: Execute callback directly without transition
|
||||||
|
const result = callback(false);
|
||||||
try {
|
return result instanceof Promise ? result : Promise.resolve();
|
||||||
// View Transitions require DOM updates to happen synchronously within
|
|
||||||
// the callback. Execute the callback immediately (synchronously).
|
|
||||||
const transition = document.startViewTransition(() => {
|
|
||||||
callbackInvoked = true;
|
|
||||||
callback(true);
|
|
||||||
});
|
|
||||||
return transition.finished;
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn(
|
|
||||||
"View transition failed, falling back to direct execution.",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
// Make sure the callback is invoked exactly once.
|
|
||||||
if (!callbackInvoked) {
|
|
||||||
callback(false);
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
import type { EChartsType } from "echarts/core";
|
|
||||||
import type { SunburstSeriesOption } from "echarts/types/dist/echarts";
|
|
||||||
import type { CallbackDataParams } from "echarts/types/src/util/types";
|
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
|
||||||
import { filterXSS } from "../../common/util/xss";
|
|
||||||
import type { ECOption } from "../../resources/echarts/echarts";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import "./ha-chart-base";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
|
||||||
let SunburstChart: typeof import("echarts/lib/chart/sunburst/install");
|
|
||||||
|
|
||||||
export interface SunburstNode {
|
|
||||||
id: string;
|
|
||||||
name?: string;
|
|
||||||
value: number;
|
|
||||||
itemStyle?: {
|
|
||||||
color?: string;
|
|
||||||
};
|
|
||||||
children?: SunburstNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-sunburst-chart")
|
|
||||||
export class HaSunburstChart extends LitElement {
|
|
||||||
public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public data?: SunburstNode;
|
|
||||||
|
|
||||||
@property({ type: String, attribute: false }) public valueFormatter?: (
|
|
||||||
value: number
|
|
||||||
) => string;
|
|
||||||
|
|
||||||
public chart?: EChartsType;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
if (!SunburstChart) {
|
|
||||||
import("echarts/lib/chart/sunburst/install").then((module) => {
|
|
||||||
SunburstChart = module;
|
|
||||||
this.requestUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!SunburstChart || !this.data) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
tooltip: {
|
|
||||||
trigger: "item",
|
|
||||||
formatter: this._renderTooltip,
|
|
||||||
appendTo: document.body,
|
|
||||||
},
|
|
||||||
} as ECOption;
|
|
||||||
|
|
||||||
return html`<ha-chart-base
|
|
||||||
.data=${this._createData(this.data)}
|
|
||||||
.options=${options}
|
|
||||||
height="100%"
|
|
||||||
.extraComponents=${[SunburstChart]}
|
|
||||||
></ha-chart-base>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderTooltip = (params: CallbackDataParams) => {
|
|
||||||
const data = params.data as { name: string; value: number };
|
|
||||||
const value = this.valueFormatter
|
|
||||||
? this.valueFormatter(data.value)
|
|
||||||
: data.value;
|
|
||||||
return `${params.marker} ${filterXSS(data.name)}<br>${value}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _createData = memoizeOne(
|
|
||||||
(data: SunburstNode): SunburstSeriesOption => {
|
|
||||||
const computedStyles = getComputedStyle(this);
|
|
||||||
|
|
||||||
// Transform to echarts format (uses 'name' instead of 'id')
|
|
||||||
const transformNode = (
|
|
||||||
node: SunburstNode,
|
|
||||||
index: number,
|
|
||||||
depth: number,
|
|
||||||
parentColor?: string
|
|
||||||
) => {
|
|
||||||
const result = {
|
|
||||||
...node,
|
|
||||||
name: node.name || node.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (depth > 0 && !node.itemStyle?.color) {
|
|
||||||
// Don't assign color to root node
|
|
||||||
result.itemStyle = {
|
|
||||||
color: parentColor ?? getGraphColorByIndex(index, computedStyles),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.children && node.children.length > 0) {
|
|
||||||
result.children = node.children.map((child, i) =>
|
|
||||||
transformNode(child, i, depth + 1, result.itemStyle?.color)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const transformedData = transformNode(data, 0, 0);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "sunburst",
|
|
||||||
data: transformedData.children || [transformedData],
|
|
||||||
radius: [0, "90%"],
|
|
||||||
sort: undefined, // Keep original order
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
align: "center",
|
|
||||||
rotate: "radial",
|
|
||||||
minAngle: 15,
|
|
||||||
hideOverlap: true,
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
focus: "ancestor",
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: 2,
|
|
||||||
},
|
|
||||||
levels: this._generateLevels(this._getMaxDepth(data)),
|
|
||||||
} as SunburstSeriesOption;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getMaxDepth(node: SunburstNode, currentDepth = 0): number {
|
|
||||||
if (!node.children || node.children.length === 0) {
|
|
||||||
return currentDepth;
|
|
||||||
}
|
|
||||||
return Math.max(
|
|
||||||
...node.children.map((child) =>
|
|
||||||
this._getMaxDepth(child, currentDepth + 1)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _generateLevels(depth: number): SunburstSeriesOption["levels"] {
|
|
||||||
const levels: SunburstSeriesOption["levels"] = [];
|
|
||||||
|
|
||||||
// Root level (center) - transparent, small fixed size
|
|
||||||
const rootRadius = 15;
|
|
||||||
const outerRadius = 95;
|
|
||||||
const availableRadius = outerRadius - rootRadius;
|
|
||||||
|
|
||||||
levels.push({
|
|
||||||
r0: "0%",
|
|
||||||
r: `${rootRadius}%`,
|
|
||||||
itemStyle: {
|
|
||||||
color: "transparent",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (depth === 0) {
|
|
||||||
return levels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Distribute remaining radius among data levels using weighted distribution
|
|
||||||
// First level gets most space, each subsequent level gets progressively smaller
|
|
||||||
const weights = Array.from({ length: depth }, (_, i) => depth - i);
|
|
||||||
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
|
|
||||||
|
|
||||||
let currentRadius = rootRadius;
|
|
||||||
for (let i = 0; i < depth; i++) {
|
|
||||||
const levelRadius = (weights[i] / totalWeight) * availableRadius;
|
|
||||||
const r0 = currentRadius;
|
|
||||||
const r = currentRadius + levelRadius;
|
|
||||||
currentRadius = r;
|
|
||||||
|
|
||||||
levels.push({
|
|
||||||
r0: `${r0}%`,
|
|
||||||
r: `${r}%`,
|
|
||||||
// Show labels only on first level
|
|
||||||
...(i === 0 ? { label: { show: true } } : {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return levels;
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
ha-chart-base {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-sunburst-chart": HaSunburstChart;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -184,11 +184,17 @@ export class StatisticsChart extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _datasetHidden(ev: CustomEvent) {
|
private _datasetHidden(ev: CustomEvent) {
|
||||||
|
if (!this._legendData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._hiddenStats.add(ev.detail.id);
|
this._hiddenStats.add(ev.detail.id);
|
||||||
this.requestUpdate("_hiddenStats");
|
this.requestUpdate("_hiddenStats");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _datasetUnhidden(ev: CustomEvent) {
|
private _datasetUnhidden(ev: CustomEvent) {
|
||||||
|
if (!this._legendData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._hiddenStats.delete(ev.detail.id);
|
this._hiddenStats.delete(ev.detail.id);
|
||||||
this.requestUpdate("_hiddenStats");
|
this.requestUpdate("_hiddenStats");
|
||||||
}
|
}
|
||||||
@@ -515,9 +521,7 @@ export class StatisticsChart extends LitElement {
|
|||||||
`ui.components.statistics_charts.statistic_types.${type}`
|
`ui.components.statistics_charts.statistic_types.${type}`
|
||||||
),
|
),
|
||||||
symbol: "none",
|
symbol: "none",
|
||||||
// minmax sampling operates independently per series, breaking stacking alignment
|
sampling: "minmax",
|
||||||
// https://github.com/apache/echarts/issues/11879
|
|
||||||
sampling: band && drawBands ? "lttb" : "minmax",
|
|
||||||
animationDurationUpdate: 0,
|
animationDurationUpdate: 0,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
@@ -535,19 +539,12 @@ export class StatisticsChart extends LitElement {
|
|||||||
if (band && this.chartType === "line") {
|
if (band && this.chartType === "line") {
|
||||||
series.stack = `band-${statistic_id}`;
|
series.stack = `band-${statistic_id}`;
|
||||||
series.stackStrategy = "all";
|
series.stackStrategy = "all";
|
||||||
if (this._hiddenStats.has(`${statistic_id}-${bandBottom}`)) {
|
|
||||||
// changing the stackOrder forces echarts to render the stacked series that are not hidden #28472
|
|
||||||
series.stackOrder = "seriesDesc";
|
|
||||||
(series as LineSeriesOption).areaStyle = undefined;
|
|
||||||
} else {
|
|
||||||
series.stackOrder = "seriesAsc";
|
|
||||||
if (drawBands && type === bandTop) {
|
if (drawBands && type === bandTop) {
|
||||||
(series as LineSeriesOption).areaStyle = {
|
(series as LineSeriesOption).areaStyle = {
|
||||||
color: color + "3F",
|
color: color + "3F",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!this.hideLegend) {
|
if (!this.hideLegend) {
|
||||||
const showLegend = hasMean
|
const showLegend = hasMean
|
||||||
? type === "mean"
|
? type === "mean"
|
||||||
@@ -589,8 +586,7 @@ export class StatisticsChart extends LitElement {
|
|||||||
} else if (
|
} else if (
|
||||||
type === bandTop &&
|
type === bandTop &&
|
||||||
this.chartType === "line" &&
|
this.chartType === "line" &&
|
||||||
drawBands &&
|
drawBands
|
||||||
!this._hiddenStats.has(`${statistic_id}-${bandBottom}`)
|
|
||||||
) {
|
) {
|
||||||
const top = stat[bandTop] || 0;
|
const top = stat[bandTop] || 0;
|
||||||
val.push(Math.abs(top - (stat[bandBottom] || 0)));
|
val.push(Math.abs(top - (stat[bandBottom] || 0)));
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { getGraphColorByIndex } from "../../common/color/colors";
|
|||||||
import { hex2rgb, lab2hex, rgb2lab } from "../../common/color/convert-color";
|
import { hex2rgb, lab2hex, rgb2lab } from "../../common/color/convert-color";
|
||||||
import { labBrighten } from "../../common/color/lab";
|
import { labBrighten } from "../../common/color/lab";
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
|
import { stateColorProperties } from "../../common/entity/state_color";
|
||||||
|
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||||
|
import { computeCssValue } from "../../resources/css-variables";
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||||
import { stateColorProperties } from "../../common/entity/state_color";
|
|
||||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
|
||||||
import { computeCssValue } from "../../resources/css-variables";
|
|
||||||
|
|
||||||
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
|
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
|
||||||
media_player: {
|
media_player: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { computeCssColor } from "../../common/color/compute-color";
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
import { stringCompare } from "../../common/string/compare";
|
import { stringCompare } from "../../common/string/compare";
|
||||||
import type { LabelRegistryEntry } from "../../data/label/label_registry";
|
import type { LabelRegistryEntry } from "../../data/label_registry";
|
||||||
import "../chips/ha-chip-set";
|
import "../chips/ha-chip-set";
|
||||||
import "../ha-dropdown";
|
import "../ha-dropdown";
|
||||||
import "../ha-dropdown-item";
|
import "../ha-dropdown-item";
|
||||||
|
|||||||
@@ -16,10 +16,8 @@ import memoizeOne from "memoize-one";
|
|||||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stringCompare } from "../../common/string/compare";
|
import { stringCompare } from "../../common/string/compare";
|
||||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import { groupBy } from "../../common/util/group-by";
|
import { groupBy } from "../../common/util/group-by";
|
||||||
import { nextRender } from "../../common/util/render-status";
|
|
||||||
import { haStyleScrollbar } from "../../resources/styles";
|
import { haStyleScrollbar } from "../../resources/styles";
|
||||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
@@ -28,6 +26,8 @@ import type { HaCheckbox } from "../ha-checkbox";
|
|||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "../search-input";
|
import "../search-input";
|
||||||
import { filterData, sortData } from "./sort-filter";
|
import { filterData, sortData } from "./sort-filter";
|
||||||
|
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||||
|
import { nextRender } from "../../common/util/render-status";
|
||||||
|
|
||||||
export interface RowClickedEvent {
|
export interface RowClickedEvent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1402,9 +1402,6 @@ export class HaDataTable extends LitElement {
|
|||||||
}
|
}
|
||||||
.secondary {
|
.secondary {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
.scroller {
|
.scroller {
|
||||||
height: calc(100% - 57px);
|
height: calc(100% - 57px);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { expose } from "comlink";
|
import { expose } from "comlink";
|
||||||
import Fuse, { type FuseOptionKey } from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ipCompare, stringCompare } from "../../common/string/compare";
|
import { ipCompare, stringCompare } from "../../common/string/compare";
|
||||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
||||||
import { multiTermSearch } from "../../resources/fuseMultiTerm";
|
import { HaFuse } from "../../resources/fuse";
|
||||||
import type {
|
import type {
|
||||||
ClonedDataTableColumnData,
|
ClonedDataTableColumnData,
|
||||||
DataTableRowData,
|
DataTableRowData,
|
||||||
@@ -11,10 +11,9 @@ import type {
|
|||||||
SortingDirection,
|
SortingDirection,
|
||||||
} from "./ha-data-table";
|
} from "./ha-data-table";
|
||||||
|
|
||||||
const getSearchKeys = memoizeOne(
|
const fuseIndex = memoizeOne(
|
||||||
(columns: SortableColumnContainer): FuseOptionKey<DataTableRowData>[] => {
|
(data: DataTableRowData[], columns: SortableColumnContainer) => {
|
||||||
const searchKeys = new Set<string>();
|
const searchKeys = new Set<string>();
|
||||||
|
|
||||||
Object.entries(columns).forEach(([key, column]) => {
|
Object.entries(columns).forEach(([key, column]) => {
|
||||||
if (column.filterable) {
|
if (column.filterable) {
|
||||||
searchKeys.add(
|
searchKeys.add(
|
||||||
@@ -24,15 +23,10 @@ const getSearchKeys = memoizeOne(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return Array.from(searchKeys);
|
return Fuse.createIndex([...searchKeys], data);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const fuseIndex = memoizeOne(
|
|
||||||
(data: DataTableRowData[], keys: FuseOptionKey<DataTableRowData>[]) =>
|
|
||||||
Fuse.createIndex(keys, data)
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterData = (
|
const filterData = (
|
||||||
data: DataTableRowData[],
|
data: DataTableRowData[],
|
||||||
columns: SortableColumnContainer,
|
columns: SortableColumnContainer,
|
||||||
@@ -44,13 +38,21 @@ const filterData = (
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = getSearchKeys(columns);
|
const index = fuseIndex(data, columns);
|
||||||
|
|
||||||
const index = fuseIndex(data, keys);
|
const fuse = new HaFuse(
|
||||||
|
data,
|
||||||
|
{ shouldSort: false, minMatchCharLength: 1 },
|
||||||
|
index
|
||||||
|
);
|
||||||
|
|
||||||
return multiTermSearch<DataTableRowData>(data, filter, keys, index, {
|
const searchResults = fuse.multiTermsSearch(filter);
|
||||||
threshold: 0.2, // reduce fuzzy matches in data tables
|
|
||||||
});
|
if (searchResults) {
|
||||||
|
return searchResults.map((result) => result.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortData = (
|
const sortData = (
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import type { DeviceAction } from "../../data/device/device_automation";
|
import type { DeviceAction } from "../../data/device_automation";
|
||||||
import {
|
import {
|
||||||
fetchDeviceActions,
|
fetchDeviceActions,
|
||||||
localizeDeviceAutomationAction,
|
localizeDeviceAutomationAction,
|
||||||
} from "../../data/device/device_automation";
|
} from "../../data/device_automation";
|
||||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||||
|
|
||||||
@customElement("ha-device-action-picker")
|
@customElement("ha-device-action-picker")
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ import { consume } from "@lit/context";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { property, state } from "lit/decorators";
|
import { property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
|
||||||
import { fullEntitiesContext } from "../../data/context";
|
import { fullEntitiesContext } from "../../data/context";
|
||||||
import type { DeviceAutomation } from "../../data/device/device_automation";
|
import type { DeviceAutomation } from "../../data/device_automation";
|
||||||
import {
|
import {
|
||||||
deviceAutomationsEqual,
|
deviceAutomationsEqual,
|
||||||
sortDeviceAutomations,
|
sortDeviceAutomations,
|
||||||
} from "../../data/device/device_automation";
|
} from "../../data/device_automation";
|
||||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-md-select";
|
|
||||||
import "../ha-md-select-option";
|
import "../ha-md-select-option";
|
||||||
|
import "../ha-md-select";
|
||||||
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
|
|
||||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||||
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
|
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import type { DeviceCondition } from "../../data/device/device_automation";
|
import type { DeviceCondition } from "../../data/device_automation";
|
||||||
import {
|
import {
|
||||||
fetchDeviceConditions,
|
fetchDeviceConditions,
|
||||||
localizeDeviceAutomationCondition,
|
localizeDeviceAutomationCondition,
|
||||||
} from "../../data/device/device_automation";
|
} from "../../data/device_automation";
|
||||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||||
|
|
||||||
@customElement("ha-device-condition-picker")
|
@customElement("ha-device-condition-picker")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
@@ -9,11 +9,10 @@ import { computeDeviceName } from "../../common/entity/compute_device_name";
|
|||||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
||||||
import {
|
import {
|
||||||
deviceComboBoxKeys,
|
|
||||||
getDevices,
|
getDevices,
|
||||||
type DevicePickerItem,
|
type DevicePickerItem,
|
||||||
} from "../../data/device/device_picker";
|
type DeviceRegistryEntry,
|
||||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
} from "../../data/device_registry";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { brandsUrl } from "../../util/brands-url";
|
import { brandsUrl } from "../../util/brands-url";
|
||||||
import "../ha-generic-picker";
|
import "../ha-generic-picker";
|
||||||
@@ -162,7 +161,7 @@ export class HaDevicePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _rowRenderer: RenderItemFunction<DevicePickerItem> = (item) => html`
|
private _rowRenderer: ComboBoxLitRenderer<DevicePickerItem> = (item) => html`
|
||||||
<ha-combo-box-item type="button">
|
<ha-combo-box-item type="button">
|
||||||
${item.domain
|
${item.domain
|
||||||
? html`
|
? html`
|
||||||
@@ -205,8 +204,6 @@ export class HaDevicePicker extends LitElement {
|
|||||||
<ha-generic-picker
|
<ha-generic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.disabled=${this.disabled}
|
|
||||||
.helper=${this.helper}
|
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.searchLabel=${this.searchLabel}
|
.searchLabel=${this.searchLabel}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
@@ -219,10 +216,6 @@ export class HaDevicePicker extends LitElement {
|
|||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
.searchKeys=${deviceComboBoxKeys}
|
|
||||||
.unknownItemText=${this.hass.localize(
|
|
||||||
"ui.components.device-picker.unknown"
|
|
||||||
)}
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import type { DeviceTrigger } from "../../data/device/device_automation";
|
import type { DeviceTrigger } from "../../data/device_automation";
|
||||||
import {
|
import {
|
||||||
fetchDeviceTriggers,
|
fetchDeviceTriggers,
|
||||||
localizeDeviceAutomationTrigger,
|
localizeDeviceAutomationTrigger,
|
||||||
} from "../../data/device/device_automation";
|
} from "../../data/device_automation";
|
||||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||||
|
|
||||||
@customElement("ha-device-trigger-picker")
|
@customElement("ha-device-trigger-picker")
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class HaDevicesPicker extends LitElement {
|
|||||||
(entityId) => html`
|
(entityId) => html`
|
||||||
<div>
|
<div>
|
||||||
<ha-device-picker
|
<ha-device-picker
|
||||||
|
allow-custom-entity
|
||||||
.curValue=${entityId}
|
.curValue=${entityId}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.deviceFilter=${this.deviceFilter}
|
.deviceFilter=${this.deviceFilter}
|
||||||
@@ -78,6 +79,7 @@ class HaDevicesPicker extends LitElement {
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<ha-device-picker
|
<ha-device-picker
|
||||||
|
allow-custom-entity
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.deviceFilter=${this.deviceFilter}
|
.deviceFilter=${this.deviceFilter}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
import "../ha-sortable";
|
import "../ha-sortable";
|
||||||
import "./ha-entity-picker";
|
import "./ha-entity-picker";
|
||||||
@@ -99,6 +99,7 @@ class HaEntitiesPicker extends LitElement {
|
|||||||
(entityId) => html`
|
(entityId) => html`
|
||||||
<div class="entity">
|
<div class="entity">
|
||||||
<ha-entity-picker
|
<ha-entity-picker
|
||||||
|
allow-custom-entity
|
||||||
.curValue=${entityId}
|
.curValue=${entityId}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
@@ -128,6 +129,7 @@ class HaEntitiesPicker extends LitElement {
|
|||||||
</ha-sortable>
|
</ha-sortable>
|
||||||
<div>
|
<div>
|
||||||
<ha-entity-picker
|
<ha-entity-picker
|
||||||
|
allow-custom-entity
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
.excludeDomains=${this.excludeDomains}
|
.excludeDomains=${this.excludeDomains}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
import type { PropertyValues } from "lit";
|
||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
import "../ha-generic-picker";
|
import "../ha-combo-box";
|
||||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
|
|
||||||
|
interface AttributeOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("ha-entity-attribute-picker")
|
@customElement("ha-entity-attribute-picker")
|
||||||
class HaEntityAttributePicker extends LitElement {
|
class HaEntityAttributePicker extends LitElement {
|
||||||
@@ -37,44 +42,51 @@ class HaEntityAttributePicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
private _getItemsMemoized = memoizeOne(
|
@state() private _opened = false;
|
||||||
(
|
|
||||||
entityId: string | string[] | undefined,
|
|
||||||
hideAttributes: string[] | undefined,
|
|
||||||
hass: HomeAssistant
|
|
||||||
): PickerComboBoxItem[] => {
|
|
||||||
const entityIds = entityId ? ensureArray(entityId) : [];
|
|
||||||
const options: PickerComboBoxItem[] = [];
|
|
||||||
const optionsSet = new Set<string>();
|
|
||||||
|
|
||||||
for (const id of entityIds) {
|
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||||
const stateObj = hass.states[id];
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues) {
|
||||||
|
return !(!changedProps.has("_opened") && this._opened);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
(changedProps.has("_opened") && this._opened) ||
|
||||||
|
changedProps.has("entityId") ||
|
||||||
|
changedProps.has("attribute")
|
||||||
|
) {
|
||||||
|
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
||||||
|
const entitiesOptions = entityIds.map<AttributeOption[]>((entityId) => {
|
||||||
|
const stateObj = this.hass.states[entityId];
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
continue;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributes = Object.keys(stateObj.attributes).filter(
|
const attributes = Object.keys(stateObj.attributes).filter(
|
||||||
(a) => !hideAttributes?.includes(a)
|
(a) => !this.hideAttributes?.includes(a)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const attribute of attributes) {
|
return attributes.map((a) => ({
|
||||||
if (!optionsSet.has(attribute)) {
|
value: a,
|
||||||
optionsSet.add(attribute);
|
label: this.hass.formatEntityAttributeName(stateObj, a),
|
||||||
options.push({
|
}));
|
||||||
id: attribute,
|
|
||||||
primary: hass.formatEntityAttributeName(stateObj, attribute),
|
|
||||||
sorting_label: attribute,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const options: AttributeOption[] = [];
|
||||||
|
const optionsSet = new Set<string>();
|
||||||
|
for (const entityOptions of entitiesOptions) {
|
||||||
|
for (const option of entityOptions) {
|
||||||
|
if (!optionsSet.has(option.value)) {
|
||||||
|
optionsSet.add(option.value);
|
||||||
|
options.push(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
(this._comboBox as any).filteredItems = options;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
private _getItems = () =>
|
|
||||||
this._getItemsMemoized(this.entityId, this.hideAttributes, this.hass);
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
@@ -82,9 +94,10 @@ class HaEntityAttributePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label ??
|
.label=${this.label ??
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
"ui.components.entity.entity-attribute-picker.attribute"
|
"ui.components.entity.entity-attribute-picker.attribute"
|
||||||
@@ -93,22 +106,39 @@ class HaEntityAttributePicker extends LitElement {
|
|||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
.getItems=${this._getItems}
|
item-id-path="value"
|
||||||
|
item-value-path="value"
|
||||||
|
item-label-path="label"
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const newValue = ev.detail.value;
|
const newValue = ev.detail.value;
|
||||||
if (newValue !== this.value) {
|
if (newValue !== this._value) {
|
||||||
this.value = newValue;
|
this._setValue(newValue);
|
||||||
fireEvent(this, "value-changed", { value: newValue });
|
|
||||||
fireEvent(this, "change");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _setValue(value: string) {
|
||||||
|
this.value = value;
|
||||||
|
setTimeout(() => {
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import type { IFuseOptions } from "fuse.js";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
|
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
|
||||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||||
import type { EntityNameType } from "../../common/translations/entity-state";
|
import type { EntityNameType } from "../../common/translations/entity-state";
|
||||||
@@ -14,18 +18,20 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
|||||||
import "../chips/ha-assist-chip";
|
import "../chips/ha-assist-chip";
|
||||||
import "../chips/ha-chip-set";
|
import "../chips/ha-chip-set";
|
||||||
import "../chips/ha-input-chip";
|
import "../chips/ha-input-chip";
|
||||||
import "../ha-combo-box-item";
|
import "../ha-combo-box";
|
||||||
import "../ha-generic-picker";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
|
||||||
import "../ha-input-helper-text";
|
import "../ha-input-helper-text";
|
||||||
import {
|
|
||||||
NO_ITEMS_AVAILABLE_ID,
|
|
||||||
type PickerComboBoxItem,
|
|
||||||
} from "../ha-picker-combo-box";
|
|
||||||
import "../ha-sortable";
|
import "../ha-sortable";
|
||||||
|
|
||||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
interface EntityNameOption {
|
||||||
<ha-combo-box-item type="button" compact>
|
primary: string;
|
||||||
|
secondary?: string;
|
||||||
|
field_label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowRenderer: ComboBoxLitRenderer<EntityNameOption> = (item) => html`
|
||||||
|
<ha-combo-box-item type="button">
|
||||||
<span slot="headline">${item.primary}</span>
|
<span slot="headline">${item.primary}</span>
|
||||||
${item.secondary
|
${item.secondary
|
||||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||||
@@ -73,7 +79,11 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||||
|
|
||||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
@query(".container", true) private _container?: HTMLDivElement;
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
@state() private _opened = false;
|
||||||
|
|
||||||
private _editIndex?: number;
|
private _editIndex?: number;
|
||||||
|
|
||||||
@@ -105,7 +115,7 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
return options;
|
return options;
|
||||||
});
|
});
|
||||||
|
|
||||||
private _getItems = memoizeOne((entityId?: string) => {
|
private _getOptions = memoizeOne((entityId?: string) => {
|
||||||
if (!entityId) {
|
if (!entityId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -114,7 +124,7 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
|
|
||||||
const items = (
|
const items = (
|
||||||
["entity", "device", "area", "floor"] as const
|
["entity", "device", "area", "floor"] as const
|
||||||
).map<PickerComboBoxItem>((name) => {
|
).map<EntityNameOption>((name) => {
|
||||||
const stateObj = this.hass.states[entityId];
|
const stateObj = this.hass.states[entityId];
|
||||||
const isValid = types.has(name);
|
const isValid = types.has(name);
|
||||||
const primary = this.hass.localize(
|
const primary = this.hass.localize(
|
||||||
@@ -127,39 +137,25 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
|
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
|
||||||
)) || "-";
|
)) || "-";
|
||||||
|
|
||||||
const id = formatOptionValue({ type: name });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
|
||||||
primary,
|
primary,
|
||||||
secondary,
|
secondary,
|
||||||
search_labels: {
|
field_label: primary,
|
||||||
primary,
|
value: formatOptionValue({ type: name }),
|
||||||
secondary: secondary || null,
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
sorting_label: primary,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
});
|
});
|
||||||
|
|
||||||
private _customNameOption = memoizeOne(
|
private _customNameOption = memoizeOne((text: string) => ({
|
||||||
(text: string): PickerComboBoxItem => ({
|
|
||||||
id: formatOptionValue({ type: "text", text }),
|
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
"ui.components.entity.entity-name-picker.custom_name"
|
"ui.components.entity.entity-name-picker.custom_name"
|
||||||
),
|
),
|
||||||
secondary: `"${text}"`,
|
secondary: `"${text}"`,
|
||||||
search_labels: {
|
field_label: text,
|
||||||
primary: text,
|
value: formatOptionValue({ type: "text", text }),
|
||||||
secondary: `"${text}"`,
|
}));
|
||||||
id: formatOptionValue({ type: "text", text }),
|
|
||||||
},
|
|
||||||
sorting_label: text,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
private _formatItem = (item: EntityNameItem) => {
|
private _formatItem = (item: EntityNameItem) => {
|
||||||
if (item.type === "text") {
|
if (item.type === "text") {
|
||||||
@@ -175,29 +171,12 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const value = this._items;
|
const value = this._items;
|
||||||
|
const options = this._getOptions(this.entityId);
|
||||||
const validTypes = this._validTypes(this.entityId);
|
const validTypes = this._validTypes(this.entityId);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
<ha-generic-picker
|
<div class="container">
|
||||||
.hass=${this.hass}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
.required=${this.required && !value.length}
|
|
||||||
.getItems=${this._getFilteredItems}
|
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
|
||||||
.rowRenderer=${rowRenderer}
|
|
||||||
.searchFn=${this._searchFn}
|
|
||||||
.notFoundLabel=${this.hass.localize(
|
|
||||||
"ui.components.entity.entity-name-picker.no_match"
|
|
||||||
)}
|
|
||||||
.value=${this._getPickerValue()}
|
|
||||||
allow-custom-value
|
|
||||||
.customValueLabel=${this.hass.localize(
|
|
||||||
"ui.components.entity.entity-name-picker.custom_name"
|
|
||||||
)}
|
|
||||||
@value-changed=${this._pickerValueChanged}
|
|
||||||
>
|
|
||||||
<div slot="field" class="container">
|
|
||||||
<ha-sortable
|
<ha-sortable
|
||||||
no-style
|
no-style
|
||||||
@item-moved=${this._moveItem}
|
@item-moved=${this._moveItem}
|
||||||
@@ -247,8 +226,33 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
`}
|
`}
|
||||||
</ha-chip-set>
|
</ha-chip-set>
|
||||||
</ha-sortable>
|
</ha-sortable>
|
||||||
|
|
||||||
|
<mwc-menu-surface
|
||||||
|
.open=${this._opened}
|
||||||
|
@closed=${this._onClosed}
|
||||||
|
@opened=${this._onOpened}
|
||||||
|
@input=${stopPropagation}
|
||||||
|
.anchor=${this._container}
|
||||||
|
>
|
||||||
|
<ha-combo-box
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${""}
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required && !value.length}
|
||||||
|
.items=${options}
|
||||||
|
allow-custom-value
|
||||||
|
item-id-path="value"
|
||||||
|
item-value-path="value"
|
||||||
|
item-label-path="field_label"
|
||||||
|
.renderer=${rowRenderer}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._comboBoxValueChanged}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
>
|
||||||
|
</ha-combo-box>
|
||||||
|
</mwc-menu-surface>
|
||||||
</div>
|
</div>
|
||||||
</ha-generic-picker>
|
|
||||||
${this._renderHelper()}
|
${this._renderHelper()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -263,22 +267,32 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
: nothing;
|
: nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _addItem(ev: Event) {
|
private _onClosed(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
this._opened = false;
|
||||||
this._editIndex = undefined;
|
this._editIndex = undefined;
|
||||||
await this.updateComplete;
|
|
||||||
await this._picker?.open();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _editItem(ev: Event) {
|
private async _onOpened(ev) {
|
||||||
|
if (!this._opened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const idx = parseInt(
|
this._opened = true;
|
||||||
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
await this._comboBox?.focus();
|
||||||
10
|
await this._comboBox?.open();
|
||||||
);
|
}
|
||||||
|
|
||||||
|
private async _addItem(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._opened = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _editItem(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
|
||||||
this._editIndex = idx;
|
this._editIndex = idx;
|
||||||
await this.updateComplete;
|
this._opened = true;
|
||||||
await this._picker?.open();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _items(): EntityNameItem[] {
|
private get _items(): EntityNameItem[] {
|
||||||
@@ -308,80 +322,78 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _getPickerValue(): string | undefined {
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
if (this._editIndex != null) {
|
const open = ev.detail.value;
|
||||||
const item = this._items[this._editIndex];
|
if (open) {
|
||||||
return item ? formatOptionValue(item) : undefined;
|
const options = this._comboBox.items || [];
|
||||||
}
|
|
||||||
return undefined;
|
const initialItem =
|
||||||
|
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
||||||
|
|
||||||
|
const initialValue = initialItem ? formatOptionValue(initialItem) : "";
|
||||||
|
|
||||||
|
const filteredItems = this._filterSelectedOptions(options, initialValue);
|
||||||
|
|
||||||
|
if (initialItem?.type === "text" && initialItem.text) {
|
||||||
|
filteredItems.push(this._customNameOption(initialItem.text));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getFilteredItems = (
|
this._comboBox.filteredItems = filteredItems;
|
||||||
searchString?: string,
|
this._comboBox.setInputValue(initialValue);
|
||||||
_section?: string
|
} else {
|
||||||
): PickerComboBoxItem[] => {
|
this._opened = false;
|
||||||
const items = this._getItems(this.entityId);
|
this._comboBox.setInputValue("");
|
||||||
const currentItem =
|
}
|
||||||
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
}
|
||||||
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
|
|
||||||
|
private _filterSelectedOptions = (
|
||||||
|
options: EntityNameOption[],
|
||||||
|
current?: string
|
||||||
|
) => {
|
||||||
|
const items = this._items;
|
||||||
|
|
||||||
const excludedValues = new Set(
|
const excludedValues = new Set(
|
||||||
this._items
|
items
|
||||||
.filter((item) => UNIQUE_TYPES.has(item.type))
|
.filter((item) => UNIQUE_TYPES.has(item.type))
|
||||||
.map((item) => formatOptionValue(item))
|
.map((item) => formatOptionValue(item))
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredItems = items.filter(
|
const filteredOptions = options.filter(
|
||||||
(item) => !excludedValues.has(item.id) || item.id === currentValue
|
(option) => !excludedValues.has(option.value) || option.value === current
|
||||||
);
|
);
|
||||||
|
return filteredOptions;
|
||||||
// When editing an existing text item, include it in the base items
|
|
||||||
if (currentItem?.type === "text" && currentItem.text && !searchString) {
|
|
||||||
filteredItems.push(this._customNameOption(currentItem.text));
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredItems;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private _getAdditionalItems = (
|
private _filterChanged(ev: ValueChangedEvent<string>) {
|
||||||
searchString?: string
|
const input = ev.detail.value;
|
||||||
): PickerComboBoxItem[] => {
|
const filter = input?.toLowerCase() || "";
|
||||||
if (!searchString) {
|
const options = this._comboBox.items || [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentItem =
|
const currentItem =
|
||||||
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
||||||
|
|
||||||
// Don't add if it's the same as the current item being edited
|
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
|
||||||
if (
|
|
||||||
currentItem?.type === "text" &&
|
let filteredItems = this._filterSelectedOptions(options, currentValue);
|
||||||
currentItem.text &&
|
|
||||||
currentItem.text === searchString
|
if (!filter) {
|
||||||
) {
|
this._comboBox.filteredItems = filteredItems;
|
||||||
return [];
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always return custom name option when there's a search string
|
const fuseOptions: IFuseOptions<EntityNameOption> = {
|
||||||
// This prevents "No matching items found" from showing
|
keys: ["primary", "secondary", "value"],
|
||||||
return [this._customNameOption(searchString)];
|
isCaseSensitive: false,
|
||||||
|
minMatchCharLength: Math.min(filter.length, 2),
|
||||||
|
threshold: 0.2,
|
||||||
|
ignoreDiacritics: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
private _searchFn = (
|
const fuse = new Fuse(filteredItems, fuseOptions);
|
||||||
search: string,
|
filteredItems = fuse.search(filter).map((result) => result.item);
|
||||||
filteredItems: PickerComboBoxItem[],
|
filteredItems.push(this._customNameOption(input));
|
||||||
_allItems: PickerComboBoxItem[]
|
this._comboBox.filteredItems = filteredItems;
|
||||||
): PickerComboBoxItem[] => {
|
|
||||||
// Remove NO_ITEMS_AVAILABLE_ID if we have additional items (custom name option)
|
|
||||||
// This prevents "No matching items found" from showing when custom values are allowed
|
|
||||||
const hasAdditionalItems = this._getAdditionalItems(search).length > 0;
|
|
||||||
if (hasAdditionalItems) {
|
|
||||||
return filteredItems.filter(
|
|
||||||
(item) => typeof item !== "string" || item !== NO_ITEMS_AVAILABLE_ID
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return filteredItems;
|
|
||||||
};
|
|
||||||
|
|
||||||
private async _moveItem(ev: CustomEvent) {
|
private async _moveItem(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
@@ -391,21 +403,25 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
const element = newValue.splice(oldIndex, 1)[0];
|
const element = newValue.splice(oldIndex, 1)[0];
|
||||||
newValue.splice(newIndex, 0, element);
|
newValue.splice(newIndex, 0, element);
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
|
await this.updateComplete;
|
||||||
|
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _removeItem(ev: Event) {
|
private async _removeItem(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = [...this._items];
|
const value = [...this._items];
|
||||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
const idx = parseInt(ev.target.dataset.idx, 10);
|
||||||
value.splice(idx, 1);
|
value.splice(idx, 1);
|
||||||
this._setValue(value);
|
this._setValue(value);
|
||||||
|
await this.updateComplete;
|
||||||
|
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = ev.detail.value;
|
const value = ev.detail.value;
|
||||||
|
|
||||||
if (this.disabled || !value) {
|
if (this.disabled || value === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,16 +431,11 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
|
|
||||||
if (this._editIndex != null) {
|
if (this._editIndex != null) {
|
||||||
newValue[this._editIndex] = item;
|
newValue[this._editIndex] = item;
|
||||||
this._editIndex = undefined;
|
|
||||||
} else {
|
} else {
|
||||||
newValue.push(item);
|
newValue.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
|
|
||||||
if (this._picker) {
|
|
||||||
this._picker.value = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value: EntityNameItem[]) {
|
private _setValue(value: EntityNameItem[]) {
|
||||||
@@ -486,6 +497,10 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mwc-menu-surface {
|
||||||
|
--mdc-menu-min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
ha-chip-set {
|
ha-chip-set {
|
||||||
padding: var(--ha-space-2) var(--ha-space-2);
|
padding: var(--ha-space-2) var(--ha-space-2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
|
||||||
import { mdiPlus, mdiShape } from "@mdi/js";
|
import { mdiPlus, mdiShape } from "@mdi/js";
|
||||||
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@@ -7,12 +7,11 @@ import { fireEvent } from "../../common/dom/fire_event";
|
|||||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||||
import {
|
import {
|
||||||
entityComboBoxKeys,
|
|
||||||
getEntities,
|
getEntities,
|
||||||
type EntityComboBoxItem,
|
type EntityComboBoxItem,
|
||||||
} from "../../data/entity/entity_picker";
|
} from "../../data/entity_registry";
|
||||||
import { domainToName } from "../../data/integration";
|
import { domainToName } from "../../data/integration";
|
||||||
import {
|
import {
|
||||||
isHelperDomain,
|
isHelperDomain,
|
||||||
@@ -172,9 +171,9 @@ export class HaEntityPicker extends LitElement {
|
|||||||
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
|
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rowRenderer: RenderItemFunction<EntityComboBoxItem> = (
|
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
|
||||||
item,
|
item,
|
||||||
index
|
{ index }
|
||||||
) => {
|
) => {
|
||||||
const showEntityId = this._showEntityId;
|
const showEntityId = this._showEntityId;
|
||||||
|
|
||||||
@@ -228,7 +227,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
if (!createDomains?.length) {
|
if (!createDomains?.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
this.hass.loadFragmentTranslation("config");
|
|
||||||
return createDomains.map((domain) => {
|
return createDomains.map((domain) => {
|
||||||
const primary = localize(
|
const primary = localize(
|
||||||
"ui.components.entity.entity-picker.create_helper",
|
"ui.components.entity.entity-picker.create_helper",
|
||||||
@@ -236,7 +235,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
domain: isHelperDomain(domain)
|
domain: isHelperDomain(domain)
|
||||||
? localize(
|
? localize(
|
||||||
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
||||||
) || domain
|
)
|
||||||
: domainToName(localize, domain),
|
: domainToName(localize, domain),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -277,28 +276,22 @@ export class HaEntityPicker extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
.allowCustomValue=${this.allowCustomEntity}
|
||||||
.required=${this.required}
|
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.placeholder=${placeholder}
|
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.value=${this.addButton ? undefined : this.value}
|
|
||||||
.searchLabel=${this.searchLabel}
|
.searchLabel=${this.searchLabel}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
|
.placeholder=${placeholder}
|
||||||
|
.value=${this.addButton ? undefined : this.value}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
.searchFn=${this._searchFn}
|
.searchFn=${this._searchFn}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
.searchKeys=${entityComboBoxKeys}
|
@value-changed=${this._valueChanged}
|
||||||
use-top-label
|
|
||||||
.addButtonLabel=${this.addButton
|
.addButtonLabel=${this.addButton
|
||||||
? this.hass.localize("ui.components.entity.entity-picker.add")
|
? this.hass.localize("ui.components.entity.entity-picker.add")
|
||||||
: undefined}
|
: undefined}
|
||||||
.unknownItemText=${this.hass.localize(
|
|
||||||
"ui.components.entity.entity-picker.unknown"
|
|
||||||
)}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||||
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import type { IFuseOptions } from "fuse.js";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
import {
|
import {
|
||||||
STATE_DISPLAY_SPECIAL_CONTENT,
|
STATE_DISPLAY_SPECIAL_CONTENT,
|
||||||
@@ -15,13 +20,21 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
|||||||
import "../chips/ha-assist-chip";
|
import "../chips/ha-assist-chip";
|
||||||
import "../chips/ha-chip-set";
|
import "../chips/ha-chip-set";
|
||||||
import "../chips/ha-input-chip";
|
import "../chips/ha-input-chip";
|
||||||
import "../ha-combo-box-item";
|
import "../ha-combo-box";
|
||||||
import "../ha-generic-picker";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
|
||||||
import "../ha-input-helper-text";
|
|
||||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
|
||||||
import "../ha-sortable";
|
import "../ha-sortable";
|
||||||
|
|
||||||
|
interface StateContentOption {
|
||||||
|
primary: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowRenderer: ComboBoxLitRenderer<StateContentOption> = (item) => html`
|
||||||
|
<ha-combo-box-item type="button">
|
||||||
|
<span slot="headline">${item.primary}</span>
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
|
||||||
const HIDDEN_ATTRIBUTES = [
|
const HIDDEN_ATTRIBUTES = [
|
||||||
"access_token",
|
"access_token",
|
||||||
"available_modes",
|
"available_modes",
|
||||||
@@ -98,88 +111,63 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
@query(".container", true) private _container?: HTMLDivElement;
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
@state() private _opened = false;
|
||||||
|
|
||||||
private _editIndex?: number;
|
private _editIndex?: number;
|
||||||
|
|
||||||
private _getItems = memoizeOne(
|
private _options = memoizeOne(
|
||||||
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
|
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
|
||||||
const domain = entityId ? computeDomain(entityId) : undefined;
|
const domain = entityId ? computeDomain(entityId) : undefined;
|
||||||
const items: PickerComboBoxItem[] = [
|
return [
|
||||||
{
|
{
|
||||||
id: "state",
|
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
"ui.components.state-content-picker.state"
|
"ui.components.state-content-picker.state"
|
||||||
),
|
),
|
||||||
sorting_label: this.hass.localize(
|
value: "state",
|
||||||
"ui.components.state-content-picker.state"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
...(allowName
|
...(allowName
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id: "name",
|
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
"ui.components.state-content-picker.name"
|
"ui.components.state-content-picker.name"
|
||||||
),
|
),
|
||||||
sorting_label: this.hass.localize(
|
value: "name",
|
||||||
"ui.components.state-content-picker.name"
|
},
|
||||||
),
|
|
||||||
} satisfies PickerComboBoxItem,
|
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
id: "last_changed",
|
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
"ui.components.state-content-picker.last_changed"
|
"ui.components.state-content-picker.last_changed"
|
||||||
),
|
),
|
||||||
sorting_label: this.hass.localize(
|
value: "last_changed",
|
||||||
"ui.components.state-content-picker.last_changed"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "last_updated",
|
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
"ui.components.state-content-picker.last_updated"
|
"ui.components.state-content-picker.last_updated"
|
||||||
),
|
),
|
||||||
sorting_label: this.hass.localize(
|
value: "last_updated",
|
||||||
"ui.components.state-content-picker.last_updated"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
...(domain
|
...(domain
|
||||||
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
|
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
|
||||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
|
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
|
||||||
).map(
|
).map((content) => ({
|
||||||
(content) =>
|
|
||||||
({
|
|
||||||
id: content,
|
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
`ui.components.state-content-picker.${content}`
|
`ui.components.state-content-picker.${content}`
|
||||||
),
|
),
|
||||||
sorting_label: this.hass.localize(
|
value: content,
|
||||||
`ui.components.state-content-picker.${content}`
|
}))
|
||||||
),
|
|
||||||
}) satisfies PickerComboBoxItem
|
|
||||||
)
|
|
||||||
: []),
|
: []),
|
||||||
...Object.keys(stateObj?.attributes ?? {})
|
...Object.keys(stateObj?.attributes ?? {})
|
||||||
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
|
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
|
||||||
.map(
|
.map((attribute) => ({
|
||||||
(attribute) =>
|
primary: this.hass.formatEntityAttributeName(stateObj!, attribute),
|
||||||
({
|
value: attribute,
|
||||||
id: attribute,
|
})),
|
||||||
primary: this.hass.formatEntityAttributeName(
|
] satisfies StateContentOption[];
|
||||||
stateObj!,
|
|
||||||
attribute
|
|
||||||
),
|
|
||||||
sorting_label: this.hass.formatEntityAttributeName(
|
|
||||||
stateObj!,
|
|
||||||
attribute
|
|
||||||
),
|
|
||||||
}) satisfies PickerComboBoxItem
|
|
||||||
),
|
|
||||||
];
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -190,23 +178,11 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
? this.hass.states[this.entityId]
|
? this.hass.states[this.entityId]
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const options = this._options(this.entityId, stateObj, this.allowName);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
<ha-generic-picker
|
<div class="container ${this.disabled ? "disabled" : ""}">
|
||||||
.hass=${this.hass}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
.required=${this.required && !value.length}
|
|
||||||
.value=${this._getPickerValue()}
|
|
||||||
.getItems=${this._getFilteredItems}
|
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
|
||||||
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
|
|
||||||
allow-custom-value
|
|
||||||
.customValueLabel=${this.hass.localize(
|
|
||||||
"ui.components.entity.entity-state-content-picker.custom_state"
|
|
||||||
)}
|
|
||||||
@value-changed=${this._pickerValueChanged}
|
|
||||||
>
|
|
||||||
<div slot="field" class="container">
|
|
||||||
<ha-sortable
|
<ha-sortable
|
||||||
no-style
|
no-style
|
||||||
@item-moved=${this._moveItem}
|
@item-moved=${this._moveItem}
|
||||||
@@ -219,7 +195,7 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
this._value,
|
this._value,
|
||||||
(item) => item,
|
(item) => item,
|
||||||
(item: string, idx) => {
|
(item: string, idx) => {
|
||||||
const label = this._getItemLabel(item, stateObj);
|
const label = options.find((o) => o.value === item)?.primary;
|
||||||
const isValid = !!label;
|
const isValid = !!label;
|
||||||
return html`
|
return html`
|
||||||
<ha-input-chip
|
<ha-input-chip
|
||||||
@@ -255,58 +231,69 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
`}
|
`}
|
||||||
</ha-chip-set>
|
</ha-chip-set>
|
||||||
</ha-sortable>
|
</ha-sortable>
|
||||||
|
|
||||||
|
<mwc-menu-surface
|
||||||
|
.open=${this._opened}
|
||||||
|
@closed=${this._onClosed}
|
||||||
|
@opened=${this._onOpened}
|
||||||
|
@input=${stopPropagation}
|
||||||
|
.anchor=${this._container}
|
||||||
|
>
|
||||||
|
<ha-combo-box
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${""}
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
|
.disabled=${this.disabled || !this.entityId}
|
||||||
|
.required=${this.required && !value.length}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.items=${options}
|
||||||
|
allow-custom-value
|
||||||
|
item-id-path="value"
|
||||||
|
item-value-path="value"
|
||||||
|
item-label-path="primary"
|
||||||
|
.renderer=${rowRenderer}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._comboBoxValueChanged}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
>
|
||||||
|
</ha-combo-box>
|
||||||
|
</mwc-menu-surface>
|
||||||
</div>
|
</div>
|
||||||
</ha-generic-picker>
|
|
||||||
${this._renderHelper()}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderHelper() {
|
private _onClosed(ev) {
|
||||||
return this.helper
|
|
||||||
? html`
|
|
||||||
<ha-input-helper-text .disabled=${this.disabled}>
|
|
||||||
${this.helper}
|
|
||||||
</ha-input-helper-text>
|
|
||||||
`
|
|
||||||
: nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _addItem(ev: Event) {
|
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
this._opened = false;
|
||||||
this._editIndex = undefined;
|
this._editIndex = undefined;
|
||||||
await this.updateComplete;
|
|
||||||
await this._picker?.open();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _editItem(ev: Event) {
|
private async _onOpened(ev) {
|
||||||
|
if (!this._opened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const idx = parseInt(
|
this._opened = true;
|
||||||
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
await this._comboBox?.focus();
|
||||||
10
|
await this._comboBox?.open();
|
||||||
);
|
}
|
||||||
|
|
||||||
|
private async _addItem(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._opened = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _editItem(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
|
||||||
this._editIndex = idx;
|
this._editIndex = idx;
|
||||||
await this.updateComplete;
|
this._opened = true;
|
||||||
await this._picker?.open();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _value() {
|
private get _value() {
|
||||||
return !this.value ? [] : ensureArray(this.value);
|
return !this.value ? [] : ensureArray(this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getItemLabel = memoizeOne(
|
|
||||||
(value: string, stateObj?: HassEntity): string | undefined => {
|
|
||||||
const stateObjForItems = this.entityId
|
|
||||||
? this.hass.states[this.entityId]
|
|
||||||
: stateObj;
|
|
||||||
const items = this._getItems(
|
|
||||||
this.entityId,
|
|
||||||
stateObjForItems,
|
|
||||||
this.allowName
|
|
||||||
);
|
|
||||||
return items.find((item) => item.id === value)?.primary;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _toValue = memoizeOne((value: string[]): typeof this.value => {
|
private _toValue = memoizeOne((value: string[]): typeof this.value => {
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -317,88 +304,64 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
|
|
||||||
private _getPickerValue(): string | undefined {
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
if (this._editIndex != null) {
|
const open = ev.detail.value;
|
||||||
return this._value[this._editIndex];
|
if (open) {
|
||||||
|
const options = this._comboBox.items || [];
|
||||||
|
|
||||||
|
const initialValue =
|
||||||
|
this._editIndex != null ? this._value[this._editIndex] : "";
|
||||||
|
const filteredItems = this._filterSelectedOptions(options, initialValue);
|
||||||
|
|
||||||
|
this._comboBox.filteredItems = filteredItems;
|
||||||
|
this._comboBox.setInputValue(initialValue);
|
||||||
|
} else {
|
||||||
|
this._opened = false;
|
||||||
}
|
}
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _customValueOption = memoizeOne(
|
private _filterSelectedOptions = (
|
||||||
(text: string): PickerComboBoxItem => ({
|
options: StateContentOption[],
|
||||||
id: text,
|
current?: string
|
||||||
primary: this.hass.localize(
|
) => {
|
||||||
"ui.components.entity.entity-state-content-picker.custom_state"
|
|
||||||
),
|
|
||||||
secondary: `"${text}"`,
|
|
||||||
search_labels: {
|
|
||||||
primary: text,
|
|
||||||
secondary: `"${text}"`,
|
|
||||||
id: text,
|
|
||||||
},
|
|
||||||
sorting_label: text,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getFilteredItems = (
|
|
||||||
searchString?: string,
|
|
||||||
_section?: string
|
|
||||||
): PickerComboBoxItem[] => {
|
|
||||||
const stateObj = this.entityId
|
|
||||||
? this.hass.states[this.entityId]
|
|
||||||
: undefined;
|
|
||||||
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
|
||||||
const currentValue =
|
|
||||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
|
||||||
|
|
||||||
const value = this._value;
|
const value = this._value;
|
||||||
|
|
||||||
const filteredItems = items.filter(
|
return options.filter(
|
||||||
(item) => !value.includes(item.id) || item.id === currentValue
|
(option) => !value.includes(option.value) || option.value === current
|
||||||
);
|
);
|
||||||
|
|
||||||
// When editing an existing custom value, include it in the base items
|
|
||||||
if (
|
|
||||||
currentValue &&
|
|
||||||
!items.find((item) => item.id === currentValue) &&
|
|
||||||
!searchString
|
|
||||||
) {
|
|
||||||
filteredItems.push(this._customValueOption(currentValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredItems;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private _getAdditionalItems = (
|
private _filterChanged(ev: ValueChangedEvent<string>) {
|
||||||
searchString?: string
|
const input = ev.detail.value;
|
||||||
): PickerComboBoxItem[] => {
|
const filter = input?.toLowerCase() || "";
|
||||||
if (!searchString) {
|
const options = this._comboBox.items || [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentValue =
|
const currentValue =
|
||||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
this._editIndex != null ? this._value[this._editIndex] : "";
|
||||||
|
|
||||||
// Don't add if it's the same as the current item being edited
|
this._comboBox.filteredItems = this._filterSelectedOptions(
|
||||||
if (currentValue && currentValue === searchString) {
|
options,
|
||||||
return [];
|
currentValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!filter) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the search string matches an existing item
|
const fuseOptions: IFuseOptions<StateContentOption> = {
|
||||||
const stateObj = this.entityId
|
keys: ["primary", "secondary", "value"],
|
||||||
? this.hass.states[this.entityId]
|
isCaseSensitive: false,
|
||||||
: undefined;
|
minMatchCharLength: Math.min(filter.length, 2),
|
||||||
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
threshold: 0.2,
|
||||||
const existingItem = items.find((item) => item.id === searchString);
|
ignoreDiacritics: true,
|
||||||
|
|
||||||
// Only return custom value option if it doesn't match an existing item
|
|
||||||
if (!existingItem) {
|
|
||||||
return [this._customValueOption(searchString)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
|
||||||
|
const filteredItems = fuse.search(filter).map((result) => result.item);
|
||||||
|
|
||||||
|
this._comboBox.filteredItems = filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
private async _moveItem(ev: CustomEvent) {
|
private async _moveItem(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const { oldIndex, newIndex } = ev.detail;
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
@@ -407,21 +370,25 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
const element = newValue.splice(oldIndex, 1)[0];
|
const element = newValue.splice(oldIndex, 1)[0];
|
||||||
newValue.splice(newIndex, 0, element);
|
newValue.splice(newIndex, 0, element);
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
|
await this.updateComplete;
|
||||||
|
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _removeItem(ev: Event) {
|
private async _removeItem(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = [...this._value];
|
const value = [...this._value];
|
||||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
const idx = parseInt(ev.target.dataset.idx, 10);
|
||||||
value.splice(idx, 1);
|
value.splice(idx, 1);
|
||||||
this._setValue(value);
|
this._setValue(value);
|
||||||
|
await this.updateComplete;
|
||||||
|
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = ev.detail.value;
|
const value = ev.detail.value;
|
||||||
|
|
||||||
if (this.disabled || !value) {
|
if (this.disabled || value === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,16 +396,11 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
|
|
||||||
if (this._editIndex != null) {
|
if (this._editIndex != null) {
|
||||||
newValue[this._editIndex] = value;
|
newValue[this._editIndex] = value;
|
||||||
this._editIndex = undefined;
|
|
||||||
} else {
|
} else {
|
||||||
newValue.push(value);
|
newValue.push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
|
|
||||||
if (this._picker) {
|
|
||||||
this._picker.value = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value: string[]) {
|
private _setValue(value: string[]) {
|
||||||
@@ -480,7 +442,7 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
height 180ms ease-in-out,
|
height 180ms ease-in-out,
|
||||||
background-color 180ms ease-in-out;
|
background-color 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
:host([disabled]) .container:after {
|
.container.disabled:after {
|
||||||
background-color: var(
|
background-color: var(
|
||||||
--mdc-text-field-disabled-line-color,
|
--mdc-text-field-disabled-line-color,
|
||||||
rgba(0, 0, 0, 0.42)
|
rgba(0, 0, 0, 0.42)
|
||||||
@@ -500,6 +462,10 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mwc-menu-surface {
|
||||||
|
--mdc-menu-min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
ha-chip-set {
|
ha-chip-set {
|
||||||
padding: var(--ha-space-2) var(--ha-space-2);
|
padding: var(--ha-space-2) var(--ha-space-2);
|
||||||
}
|
}
|
||||||
@@ -520,11 +486,6 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
.sortable-drag {
|
.sortable-drag {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-input-helper-text {
|
|
||||||
display: block;
|
|
||||||
margin: var(--ha-space-2) 0 0;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
|
import type { PropertyValues } from "lit";
|
||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { getStates } from "../../common/entity/get_states";
|
import { getStates } from "../../common/entity/get_states";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
import "../ha-generic-picker";
|
import "../ha-combo-box";
|
||||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
|
|
||||||
|
interface StateOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("ha-entity-state-picker")
|
@customElement("ha-entity-state-picker")
|
||||||
export class HaEntityStatePicker extends LitElement {
|
class HaEntityStatePicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public entityId?: string | string[];
|
@property({ attribute: false }) public entityId?: string | string[];
|
||||||
|
|
||||||
@property() public attribute?: string;
|
@property() public attribute?: string;
|
||||||
|
|
||||||
@property({ attribute: false }) public extraOptions?: PickerComboBoxItem[];
|
@property({ attribute: false }) public extraOptions?: any[];
|
||||||
|
|
||||||
// eslint-disable-next-line lit/no-native-attributes
|
// eslint-disable-next-line lit/no-native-attributes
|
||||||
@property({ type: Boolean }) public autofocus = false;
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
@@ -37,76 +42,59 @@ export class HaEntityStatePicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
private _getItems = memoizeOne(
|
@state() private _opened = false;
|
||||||
(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entityId: string | string[] | undefined,
|
|
||||||
attribute: string | undefined,
|
|
||||||
hideStates: string[] | undefined,
|
|
||||||
extraOptions: PickerComboBoxItem[] | undefined
|
|
||||||
): PickerComboBoxItem[] => {
|
|
||||||
const entityIds = entityId ? ensureArray(entityId) : [];
|
|
||||||
|
|
||||||
const entitiesOptions = entityIds.map<PickerComboBoxItem[]>(
|
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||||
(entityIdItem) => {
|
|
||||||
const stateObj = hass.states[entityIdItem] || {
|
protected shouldUpdate(changedProps: PropertyValues) {
|
||||||
entity_id: entityIdItem,
|
return !(!changedProps.has("_opened") && this._opened);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
(changedProps.has("_opened") && this._opened) ||
|
||||||
|
changedProps.has("entityId") ||
|
||||||
|
changedProps.has("attribute") ||
|
||||||
|
changedProps.has("extraOptions")
|
||||||
|
) {
|
||||||
|
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
||||||
|
|
||||||
|
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
|
||||||
|
const stateObj = this.hass.states[entityId] || {
|
||||||
|
entity_id: entityId,
|
||||||
attributes: {},
|
attributes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const states = getStates(hass, stateObj, attribute).filter(
|
const states = getStates(this.hass, stateObj, this.attribute).filter(
|
||||||
(s) => !hideStates?.includes(s)
|
(s) => !this.hideStates?.includes(s)
|
||||||
);
|
);
|
||||||
|
|
||||||
return states
|
return states.map((s) => ({
|
||||||
.map((s) => {
|
value: s,
|
||||||
const primary = attribute
|
label: this.attribute
|
||||||
? hass.formatEntityAttributeValue(stateObj, attribute, s)
|
? this.hass.formatEntityAttributeValue(stateObj, this.attribute, s)
|
||||||
: hass.formatEntityState(stateObj, s);
|
: this.hass.formatEntityState(stateObj, s),
|
||||||
return {
|
}));
|
||||||
id: s,
|
});
|
||||||
primary,
|
|
||||||
sorting_label: primary,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((option) => option.id && option.primary);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const options: PickerComboBoxItem[] = [];
|
const options: StateOption[] = [];
|
||||||
const optionsSet = new Set<string>();
|
const optionsSet = new Set<string>();
|
||||||
for (const entityOptions of entitiesOptions) {
|
for (const entityOptions of entitiesOptions) {
|
||||||
for (const option of entityOptions) {
|
for (const option of entityOptions) {
|
||||||
if (!optionsSet.has(option.id)) {
|
if (!optionsSet.has(option.value)) {
|
||||||
optionsSet.add(option.id);
|
optionsSet.add(option.value);
|
||||||
options.push(option);
|
options.push(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extraOptions) {
|
if (this.extraOptions) {
|
||||||
// Filter out any extraOptions with empty primary or id fields
|
options.unshift(...this.extraOptions);
|
||||||
const validExtraOptions = extraOptions.filter(
|
|
||||||
(option) => option.id && option.primary
|
|
||||||
);
|
|
||||||
options.unshift(...validExtraOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
(this._comboBox as any).filteredItems = options;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
private _getFilteredItems = (
|
|
||||||
_searchString?: string,
|
|
||||||
_section?: string
|
|
||||||
): PickerComboBoxItem[] =>
|
|
||||||
this._getItems(
|
|
||||||
this.hass,
|
|
||||||
this.entityId,
|
|
||||||
this.attribute,
|
|
||||||
this.hideStates,
|
|
||||||
this.extraOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
@@ -114,39 +102,48 @@ export class HaEntityStatePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.value=${this._value}
|
||||||
.disabled=${this.disabled || !this.entityId}
|
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.required=${this.required}
|
|
||||||
.label=${this.label ??
|
.label=${this.label ??
|
||||||
this.hass.localize("ui.components.entity.entity-state-picker.state")}
|
this.hass.localize("ui.components.entity.entity-state-picker.state")}
|
||||||
|
.disabled=${this.disabled || !this.entityId}
|
||||||
|
.required=${this.required}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.value=${this.value}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
.getItems=${this._getFilteredItems}
|
item-id-path="value"
|
||||||
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
|
item-value-path="value"
|
||||||
.customValueLabel=${this.hass.localize(
|
item-label-path="label"
|
||||||
"ui.components.entity.entity-state-picker.add_custom_state"
|
@opened-changed=${this._openedChanged}
|
||||||
)}
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const newValue = ev.detail.value;
|
const newValue = ev.detail.value;
|
||||||
if (newValue !== this.value) {
|
if (newValue !== this._value) {
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value: string | undefined) {
|
private _setValue(value: string) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
setTimeout(() => {
|
||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { STATES_OFF } from "../../common/const";
|
import { STATES_OFF } from "../../common/const";
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
import {
|
import { UNAVAILABLE, UNKNOWN, isUnavailableState } from "../../data/entity";
|
||||||
UNAVAILABLE,
|
|
||||||
UNKNOWN,
|
|
||||||
isUnavailableState,
|
|
||||||
} from "../../data/entity/entity";
|
|
||||||
import { forwardHaptic } from "../../data/haptics";
|
import { forwardHaptic } from "../../data/haptics";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-formfield";
|
import "../ha-formfield";
|
||||||
|
|||||||
@@ -14,12 +14,8 @@ import {
|
|||||||
getNumberFormatOptions,
|
getNumberFormatOptions,
|
||||||
isNumericState,
|
isNumericState,
|
||||||
} from "../../common/number/format_number";
|
} from "../../common/number/format_number";
|
||||||
import {
|
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||||
isUnavailableState,
|
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||||
UNAVAILABLE,
|
|
||||||
UNKNOWN,
|
|
||||||
} from "../../data/entity/entity";
|
|
||||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
|
||||||
import { timerTimeRemaining } from "../../data/timer";
|
import { timerTimeRemaining } from "../../data/timer";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-label-badge";
|
import "../ha-label-badge";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
|
||||||
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
|
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
|
||||||
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
@@ -38,21 +38,9 @@ type StatisticItemType = "entity" | "external" | "no_state";
|
|||||||
interface StatisticComboBoxItem extends PickerComboBoxItem {
|
interface StatisticComboBoxItem extends PickerComboBoxItem {
|
||||||
statistic_id?: string;
|
statistic_id?: string;
|
||||||
stateObj?: HassEntity;
|
stateObj?: HassEntity;
|
||||||
domainName?: string;
|
|
||||||
type?: StatisticItemType;
|
type?: StatisticItemType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEARCH_KEYS = [
|
|
||||||
{ name: "label", weight: 10 },
|
|
||||||
{ name: "search_labels.entityName", weight: 10 },
|
|
||||||
{ name: "search_labels.friendlyName", weight: 9 },
|
|
||||||
{ name: "search_labels.deviceName", weight: 8 },
|
|
||||||
{ name: "search_labels.areaName", weight: 6 },
|
|
||||||
{ name: "search_labels.domainName", weight: 4 },
|
|
||||||
{ name: "statisticId", weight: 3 },
|
|
||||||
{ name: "id", weight: 2 },
|
|
||||||
];
|
|
||||||
|
|
||||||
@customElement("ha-statistic-picker")
|
@customElement("ha-statistic-picker")
|
||||||
export class HaStatisticPicker extends LitElement {
|
export class HaStatisticPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -245,6 +233,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
),
|
),
|
||||||
type,
|
type,
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
|
search_labels: [label, id],
|
||||||
icon_path: mdiShape,
|
icon_path: mdiShape,
|
||||||
});
|
});
|
||||||
} else if (type === "external") {
|
} else if (type === "external") {
|
||||||
@@ -257,7 +246,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
secondary: domainName,
|
secondary: domainName,
|
||||||
type,
|
type,
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
search_labels: { label, domainName },
|
search_labels: [label, domainName, id],
|
||||||
icon_path: mdiChartLine,
|
icon_path: mdiChartLine,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -291,12 +280,13 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
stateObj: stateObj,
|
stateObj: stateObj,
|
||||||
type: "entity",
|
type: "entity",
|
||||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||||
search_labels: {
|
search_labels: [
|
||||||
entityName: entityName || null,
|
entityName,
|
||||||
deviceName: deviceName || null,
|
deviceName,
|
||||||
areaName: areaName || null,
|
areaName,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
},
|
id,
|
||||||
|
].filter(Boolean) as string[],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -371,13 +361,13 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
stateObj: stateObj,
|
stateObj: stateObj,
|
||||||
type: "entity",
|
type: "entity",
|
||||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||||
search_labels: {
|
search_labels: [
|
||||||
entityName: entityName || null,
|
entityName,
|
||||||
deviceName: deviceName || null,
|
deviceName,
|
||||||
areaName: areaName || null,
|
areaName,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
statisticId,
|
statisticId,
|
||||||
},
|
].filter(Boolean) as string[],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +394,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
secondary: domainName,
|
secondary: domainName,
|
||||||
type: "external",
|
type: "external",
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
search_labels: { label, domainName, statisticId },
|
search_labels: [label, domainName, statisticId],
|
||||||
icon_path: mdiChartLine,
|
icon_path: mdiChartLine,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -419,14 +409,14 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
|
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
|
||||||
type: "no_state",
|
type: "no_state",
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
search_labels: { label, statisticId },
|
search_labels: [label, statisticId],
|
||||||
icon_path: mdiShape,
|
icon_path: mdiShape,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rowRenderer: RenderItemFunction<StatisticComboBoxItem> = (
|
private _rowRenderer: ComboBoxLitRenderer<StatisticComboBoxItem> = (
|
||||||
item,
|
item,
|
||||||
index
|
{ index }
|
||||||
) => {
|
) => {
|
||||||
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||||
return html`
|
return html`
|
||||||
@@ -471,15 +461,13 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
.allowCustomValue=${this.allowCustomEntity}
|
||||||
.disabled=${this.disabled}
|
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
use-top-label
|
|
||||||
.placeholder=${placeholder}
|
|
||||||
.value=${this.value}
|
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
.emptyLabel=${this.hass.localize(
|
.emptyLabel=${this.hass.localize(
|
||||||
"ui.components.statistic-picker.no_statistics"
|
"ui.components.statistic-picker.no_statistics"
|
||||||
)}
|
)}
|
||||||
|
.placeholder=${placeholder}
|
||||||
|
.value=${this.value}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
@@ -487,10 +475,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
.searchFn=${this._searchFn}
|
.searchFn=${this._searchFn}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.searchKeys=${SEARCH_KEYS}
|
|
||||||
.unknownItemText=${this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.unknown"
|
|
||||||
)}
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { mdiClose } from "@mdi/js";
|
import { mdiClose } from "@mdi/js";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { listenMediaQuery } from "../common/dom/media_query";
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
import { listenMediaQuery } from "../common/dom/media_query";
|
||||||
import "./ha-bottom-sheet";
|
import "./ha-bottom-sheet";
|
||||||
import "./ha-dialog-header";
|
import "./ha-dialog-header";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
@@ -118,7 +118,6 @@ export class HaAdaptiveDialog extends LitElement {
|
|||||||
if (this._mode === "bottom-sheet") {
|
if (this._mode === "bottom-sheet") {
|
||||||
return html`
|
return html`
|
||||||
<ha-bottom-sheet .open=${this.open} flexcontent>
|
<ha-bottom-sheet .open=${this.open} flexcontent>
|
||||||
<slot name="header" slot="header">
|
|
||||||
<ha-dialog-header
|
<ha-dialog-header
|
||||||
slot="header"
|
slot="header"
|
||||||
.subtitlePosition=${this.headerSubtitlePosition}
|
.subtitlePosition=${this.headerSubtitlePosition}
|
||||||
@@ -140,7 +139,6 @@ export class HaAdaptiveDialog extends LitElement {
|
|||||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||||
</ha-dialog-header>
|
</ha-dialog-header>
|
||||||
</slot>
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<slot name="footer" slot="footer"></slot>
|
<slot name="footer" slot="footer"></slot>
|
||||||
</ha-bottom-sheet>
|
</ha-bottom-sheet>
|
||||||
@@ -159,18 +157,10 @@ export class HaAdaptiveDialog extends LitElement {
|
|||||||
.headerSubtitlePosition=${this.headerSubtitlePosition}
|
.headerSubtitlePosition=${this.headerSubtitlePosition}
|
||||||
flexcontent
|
flexcontent
|
||||||
>
|
>
|
||||||
<slot name="header" slot="header">
|
<slot name="headerNavigationIcon" slot="headerNavigationIcon"></slot>
|
||||||
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
|
|
||||||
<ha-icon-button
|
|
||||||
data-dialog="close"
|
|
||||||
.label=${this.hass.localize("ui.common.close")}
|
|
||||||
.path=${mdiClose}
|
|
||||||
></ha-icon-button>
|
|
||||||
</slot>
|
|
||||||
<slot name="headerTitle" slot="headerTitle"></slot>
|
<slot name="headerTitle" slot="headerTitle"></slot>
|
||||||
<slot name="headerSubtitle" slot="headerSubtitle"></slot>
|
<slot name="headerSubtitle" slot="headerSubtitle"></slot>
|
||||||
<slot name="headerActionItems" slot="headerActionItems"></slot>
|
<slot name="headerActionItems" slot="headerActionItems"></slot>
|
||||||
</slot>
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<slot name="footer" slot="footer"></slot>
|
<slot name="footer" slot="footer"></slot>
|
||||||
</ha-wa-dialog>
|
</ha-wa-dialog>
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import type { HassioAddonInfo } from "../data/hassio/addon";
|
||||||
import { fetchHassioAddonsInfo } from "../data/hassio/addon";
|
import { fetchHassioAddonsInfo } from "../data/hassio/addon";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import "./ha-alert";
|
import "./ha-alert";
|
||||||
|
import "./ha-combo-box";
|
||||||
|
import type { HaComboBox } from "./ha-combo-box";
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
import "./ha-generic-picker";
|
|
||||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
|
||||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
|
||||||
|
|
||||||
const SEARCH_KEYS = [
|
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`
|
||||||
{ name: "primary", weight: 10 },
|
|
||||||
{ name: "secondary", weight: 8 },
|
|
||||||
{ name: "search_labels.description", weight: 6 },
|
|
||||||
{ name: "search_labels.repository", weight: 5 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
|
||||||
<ha-combo-box-item type="button">
|
<ha-combo-box-item type="button">
|
||||||
<span slot="headline">${item.primary}</span>
|
<span slot="headline">${item.name}</span>
|
||||||
<span slot="supporting-text">${item.secondary}</span>
|
<span slot="supporting-text">${item.slug}</span>
|
||||||
${item.icon
|
${item.icon
|
||||||
? html` <img alt="" slot="start" .src=${item.icon} /> `
|
? html`
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
slot="start"
|
||||||
|
.src="/api/hassio/addons/${item.slug}/icon"
|
||||||
|
/>
|
||||||
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-combo-box-item>
|
</ha-combo-box-item>
|
||||||
`;
|
`;
|
||||||
@@ -38,22 +38,22 @@ class HaAddonPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@state() private _addons?: PickerComboBoxItem[];
|
@state() private _addons?: HassioAddonInfo[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@query("ha-generic-picker") private _genericPicker!: HaGenericPicker;
|
@query("ha-combo-box") private _comboBox!: HaComboBox;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
public open() {
|
public open() {
|
||||||
this._genericPicker?.open();
|
this._comboBox?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
this._genericPicker?.focus();
|
this._comboBox?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
@@ -61,34 +61,29 @@ class HaAddonPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const label =
|
|
||||||
this.label === undefined && this.hass
|
|
||||||
? this.hass.localize("ui.components.addon-picker.addon")
|
|
||||||
: this.label;
|
|
||||||
|
|
||||||
if (this._error) {
|
if (this._error) {
|
||||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||||
}
|
}
|
||||||
if (!this._addons) {
|
if (!this._addons) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.label=${this.label === undefined && this.hass
|
||||||
.label=${label}
|
? this.hass.localize("ui.components.addon-picker.addon")
|
||||||
.valueRenderer=${this._valueRenderer}
|
: this.label}
|
||||||
.helper=${this.helper}
|
.value=${this._value}
|
||||||
.disabled=${this.disabled}
|
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.value=${this.value}
|
.disabled=${this.disabled}
|
||||||
.getItems=${this._getItems}
|
.helper=${this.helper}
|
||||||
.searchKeys=${SEARCH_KEYS}
|
.renderer=${rowRenderer}
|
||||||
.rowRenderer=${rowRenderer}
|
.items=${this._addons}
|
||||||
|
item-value-path="slug"
|
||||||
|
item-id-path="slug"
|
||||||
|
item-label-path="name"
|
||||||
@value-changed=${this._addonChanged}
|
@value-changed=${this._addonChanged}
|
||||||
>
|
></ha-combo-box>
|
||||||
</ha-generic-picker>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,19 +93,9 @@ class HaAddonPicker extends LitElement {
|
|||||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||||
this._addons = addonsInfo.addons
|
this._addons = addonsInfo.addons
|
||||||
.filter((addon) => addon.version)
|
.filter((addon) => addon.version)
|
||||||
.map((addon) => ({
|
.sort((a, b) =>
|
||||||
id: addon.slug,
|
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||||
primary: addon.name,
|
);
|
||||||
secondary: addon.slug,
|
|
||||||
icon: addon.icon
|
|
||||||
? `/api/hassio/addons/${addon.slug}/icon`
|
|
||||||
: undefined,
|
|
||||||
search_labels: {
|
|
||||||
description: addon.description || null,
|
|
||||||
repository: addon.repository || null,
|
|
||||||
},
|
|
||||||
sorting_label: [addon.name, addon.slug].filter(Boolean).join("_"),
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
this._error = this.hass.localize(
|
this._error = this.hass.localize(
|
||||||
"ui.components.addon-picker.error.no_supervisor"
|
"ui.components.addon-picker.error.no_supervisor"
|
||||||
@@ -123,8 +108,6 @@ class HaAddonPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getItems = () => this._addons!;
|
|
||||||
|
|
||||||
private get _value() {
|
private get _value() {
|
||||||
return this.value || "";
|
return this.value || "";
|
||||||
}
|
}
|
||||||
@@ -145,17 +128,6 @@ class HaAddonPicker extends LitElement {
|
|||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueRenderer = (itemId: string) => {
|
|
||||||
const item = this._addons!.find((addon) => addon.id === itemId);
|
|
||||||
return html`${item?.icon
|
|
||||||
? html`<img
|
|
||||||
slot="start"
|
|
||||||
alt=${item.primary ?? "Unknown"}
|
|
||||||
.src=${item.icon}
|
|
||||||
/>`
|
|
||||||
: nothing}<span slot="headline">${item?.primary || "Unknown"}</span>`;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
270
src/components/ha-area-floor-picker.ts
Normal file
270
src/components/ha-area-floor-picker.ts
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import { mdiTextureBox } from "@mdi/js";
|
||||||
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import type { TemplateResult } from "lit";
|
||||||
|
import { LitElement, html, nothing } from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||||
|
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||||
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
|
import {
|
||||||
|
getAreasAndFloors,
|
||||||
|
type AreaFloorValue,
|
||||||
|
type FloorComboBoxItem,
|
||||||
|
} from "../data/area_floor";
|
||||||
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
|
import "./ha-combo-box-item";
|
||||||
|
import "./ha-floor-icon";
|
||||||
|
import "./ha-generic-picker";
|
||||||
|
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
import "./ha-tree-indicator";
|
||||||
|
|
||||||
|
const SEPARATOR = "________";
|
||||||
|
|
||||||
|
@customElement("ha-area-floor-picker")
|
||||||
|
export class HaAreaFloorPicker extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: AreaFloorValue;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ type: String, attribute: "search-label" })
|
||||||
|
public searchLabel?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only areas with entities from specific domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show no areas with entities of these domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-domains" })
|
||||||
|
public excludeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only areas with entities of these device classes.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-device-classes
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of areas to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-areas
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-areas" })
|
||||||
|
public excludeAreas?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of floors to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-floors
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-floors" })
|
||||||
|
public excludeFloors?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityFilter?: (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||||
|
|
||||||
|
public async open() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this._picker?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||||
|
const item = this._parseValue(value);
|
||||||
|
|
||||||
|
const area = item.type === "area" && this.hass.areas[value];
|
||||||
|
|
||||||
|
if (area) {
|
||||||
|
const areaName = computeAreaName(area);
|
||||||
|
return html`
|
||||||
|
${area.icon
|
||||||
|
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${mdiTextureBox}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
<slot name="headline">${areaName}</slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const floor = item.type === "floor" && this.hass.floors[value];
|
||||||
|
|
||||||
|
if (floor) {
|
||||||
|
const floorName = computeFloorName(floor);
|
||||||
|
return html`
|
||||||
|
<ha-floor-icon slot="start" .floor=${floor}></ha-floor-icon>
|
||||||
|
<span slot="headline">${floorName}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>
|
||||||
|
<span slot="headline">${value}</span>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
|
||||||
|
item,
|
||||||
|
{ index },
|
||||||
|
combobox
|
||||||
|
) => {
|
||||||
|
const nextItem = combobox.filteredItems?.[index + 1];
|
||||||
|
const isLastArea =
|
||||||
|
!nextItem ||
|
||||||
|
nextItem.type === "floor" ||
|
||||||
|
(nextItem.type === "area" && !nextItem.area?.floor_id);
|
||||||
|
|
||||||
|
const rtl = computeRTL(this.hass);
|
||||||
|
|
||||||
|
const hasFloor = item.type === "area" && item.area?.floor_id;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-combo-box-item
|
||||||
|
type="button"
|
||||||
|
style=${item.type === "area" && hasFloor
|
||||||
|
? "--md-list-item-leading-space: 48px;"
|
||||||
|
: ""}
|
||||||
|
>
|
||||||
|
${item.type === "area" && hasFloor
|
||||||
|
? html`
|
||||||
|
<ha-tree-indicator
|
||||||
|
style=${styleMap({
|
||||||
|
width: "48px",
|
||||||
|
position: "absolute",
|
||||||
|
top: "0px",
|
||||||
|
left: rtl ? undefined : "4px",
|
||||||
|
right: rtl ? "4px" : undefined,
|
||||||
|
transform: rtl ? "scaleX(-1)" : "",
|
||||||
|
})}
|
||||||
|
.end=${isLastArea}
|
||||||
|
slot="start"
|
||||||
|
></ha-tree-indicator>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${item.type === "floor" && item.floor
|
||||||
|
? html`<ha-floor-icon
|
||||||
|
slot="start"
|
||||||
|
.floor=${item.floor}
|
||||||
|
></ha-floor-icon>`
|
||||||
|
: item.icon
|
||||||
|
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${item.icon_path || mdiTextureBox}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
${item.primary}
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
|
||||||
|
|
||||||
|
private _getItems = () =>
|
||||||
|
this._getAreasAndFloorsMemoized(
|
||||||
|
this.hass.states,
|
||||||
|
this.hass.floors,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.entities,
|
||||||
|
this._formatValue,
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.entityFilter,
|
||||||
|
this.excludeAreas,
|
||||||
|
this.excludeFloors
|
||||||
|
);
|
||||||
|
|
||||||
|
private _formatValue = memoizeOne((value: AreaFloorValue): string =>
|
||||||
|
[value.type, value.id].join(SEPARATOR)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _parseValue = memoizeOne((value: string): AreaFloorValue => {
|
||||||
|
const [type, id] = value.split(SEPARATOR);
|
||||||
|
|
||||||
|
return { id, type: type as "floor" | "area" };
|
||||||
|
});
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const placeholder =
|
||||||
|
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
|
||||||
|
|
||||||
|
const value = this.value ? this._formatValue(this.value) : undefined;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-generic-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
|
.label=${this.label}
|
||||||
|
.searchLabel=${this.searchLabel}
|
||||||
|
.notFoundLabel=${this.hass.localize(
|
||||||
|
"ui.components.area-picker.no_match"
|
||||||
|
)}
|
||||||
|
.placeholder=${placeholder}
|
||||||
|
.value=${value}
|
||||||
|
.getItems=${this._getItems}
|
||||||
|
.valueRenderer=${this._valueRenderer}
|
||||||
|
.rowRenderer=${this._rowRenderer}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
</ha-generic-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const value = ev.detail.value;
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
this._setValue(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = this._parseValue(value);
|
||||||
|
this._setValue(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setValue(value?: AreaFloorValue) {
|
||||||
|
this.value = value;
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-area-floor-picker": HaAreaFloorPicker;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,9 @@ import { createAreaRegistryEntry } from "../data/area_registry";
|
|||||||
import type {
|
import type {
|
||||||
DeviceEntityDisplayLookup,
|
DeviceEntityDisplayLookup,
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
} from "../data/device/device_registry";
|
} from "../data/device_registry";
|
||||||
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
|
import { getDeviceEntityDisplayLookup } from "../data/device_registry";
|
||||||
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
|
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
@@ -30,12 +30,6 @@ import "./ha-svg-icon";
|
|||||||
|
|
||||||
const ADD_NEW_ID = "___ADD_NEW___";
|
const ADD_NEW_ID = "___ADD_NEW___";
|
||||||
|
|
||||||
const SEARCH_KEYS = [
|
|
||||||
{ name: "search_labels.areaName", weight: 10 },
|
|
||||||
{ name: "search_labels.aliases", weight: 8 },
|
|
||||||
{ name: "search_labels.floorName", weight: 6 },
|
|
||||||
{ name: "search_labels.id", weight: 3 },
|
|
||||||
];
|
|
||||||
@customElement("ha-area-picker")
|
@customElement("ha-area-picker")
|
||||||
export class HaAreaPicker extends LitElement {
|
export class HaAreaPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -296,12 +290,13 @@ export class HaAreaPicker extends LitElement {
|
|||||||
secondary: floorName,
|
secondary: floorName,
|
||||||
icon: area.icon || undefined,
|
icon: area.icon || undefined,
|
||||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||||
search_labels: {
|
sorting_label: areaName,
|
||||||
areaName: areaName || null,
|
search_labels: [
|
||||||
floorName: floorName || null,
|
areaName,
|
||||||
id: area.area_id,
|
floorName,
|
||||||
aliases: area.aliases.join(" "),
|
area.area_id,
|
||||||
},
|
...area.aliases,
|
||||||
|
].filter((v): v is string => Boolean(v)),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -363,41 +358,27 @@ export class HaAreaPicker extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const baseLabel =
|
const placeholder =
|
||||||
this.label ?? this.hass.localize("ui.components.area-picker.area");
|
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
|
||||||
const valueRenderer = this._computeValueRenderer(this.hass.areas);
|
|
||||||
|
|
||||||
// Only show label if there's no floor
|
const valueRenderer = this._computeValueRenderer(this.hass.areas);
|
||||||
let label: string | undefined = baseLabel;
|
|
||||||
if (this.value && baseLabel) {
|
|
||||||
const area = this.hass.areas[this.value];
|
|
||||||
if (area) {
|
|
||||||
const { floor } = getAreaContext(area, this.hass.floors);
|
|
||||||
if (floor) {
|
|
||||||
label = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-generic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
|
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
.addButtonLabel=${this.addButtonLabel}
|
.addButtonLabel=${this.addButtonLabel}
|
||||||
.searchKeys=${SEARCH_KEYS}
|
|
||||||
.unknownItemText=${this.hass.localize(
|
|
||||||
"ui.components.area-picker.unknown"
|
|
||||||
)}
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -659,7 +659,7 @@ export class HaAssistChat extends LitElement {
|
|||||||
--markdown-table-border-color: var(--divider-color);
|
--markdown-table-border-color: var(--divider-color);
|
||||||
--markdown-code-background-color: var(--primary-background-color);
|
--markdown-code-background-color: var(--primary-background-color);
|
||||||
--markdown-code-text-color: var(--primary-text-color);
|
--markdown-code-text-color: var(--primary-text-color);
|
||||||
--markdown-list-indent: 1.15em;
|
--markdown-list-indent: 1rem;
|
||||||
&:not(:has(ha-markdown-element)) {
|
&:not(:has(ha-markdown-element)) {
|
||||||
min-height: 1lh;
|
min-height: 1lh;
|
||||||
min-width: 1lh;
|
min-width: 1lh;
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
|
||||||
import {
|
import {
|
||||||
STATE_ATTRIBUTES,
|
STATE_ATTRIBUTES,
|
||||||
STATE_ATTRIBUTES_DOMAIN_CLASS,
|
STATE_ATTRIBUTES_DOMAIN_CLASS,
|
||||||
} from "../data/entity/entity_attributes";
|
} from "../data/entity_attributes";
|
||||||
import { haStyle } from "../resources/styles";
|
import { haStyle } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-attribute-value";
|
import "./ha-attribute-value";
|
||||||
import "./ha-expansion-panel";
|
import "./ha-expansion-panel";
|
||||||
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
|
|
||||||
@customElement("ha-attributes")
|
@customElement("ha-attributes")
|
||||||
class HaAttributes extends LitElement {
|
class HaAttributes extends LitElement {
|
||||||
|
|||||||
@@ -52,10 +52,8 @@ export class HaAutomationRow extends LitElement {
|
|||||||
<slot name="leading-icon"></slot>
|
<slot name="leading-icon"></slot>
|
||||||
</div>
|
</div>
|
||||||
<slot class="header" name="header"></slot>
|
<slot class="header" name="header"></slot>
|
||||||
<div class="icons">
|
|
||||||
<slot name="icons"></slot>
|
<slot name="icons"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,11 +118,12 @@ export class HaAutomationRow extends LitElement {
|
|||||||
}
|
}
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 var(--ha-space-3);
|
padding: var(--ha-space-0) var(--ha-space-2);
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
font-weight: var(--ha-font-weight-medium);
|
||||||
outline: none;
|
outline: none;
|
||||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||||
}
|
}
|
||||||
@@ -141,15 +140,11 @@ export class HaAutomationRow extends LitElement {
|
|||||||
background-color: var(--ha-color-fill-neutral-loud-resting);
|
background-color: var(--ha-color-fill-neutral-loud-resting);
|
||||||
border-radius: var(--ha-border-radius-md);
|
border-radius: var(--ha-border-radius-md);
|
||||||
padding: var(--ha-space-1);
|
padding: var(--ha-space-1);
|
||||||
margin-top: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
.leading-icon-wrapper {
|
|
||||||
padding-top: var(--ha-space-3);
|
|
||||||
}
|
|
||||||
::slotted([slot="leading-icon"]) {
|
::slotted([slot="leading-icon"]) {
|
||||||
color: var(--ha-color-on-neutral-quiet);
|
color: var(--ha-color-on-neutral-quiet);
|
||||||
}
|
}
|
||||||
@@ -175,11 +170,7 @@ export class HaAutomationRow extends LitElement {
|
|||||||
::slotted([slot="header"]) {
|
::slotted([slot="header"]) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
margin: 0 var(--ha-space-3);
|
margin: var(--ha-space-0) var(--ha-space-3);
|
||||||
}
|
|
||||||
.icons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
:host([sort-selected]) .row {
|
:host([sort-selected]) .row {
|
||||||
outline: solid;
|
outline: solid;
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
slot[name="footer"] {
|
slot[name="footer"] {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0;
|
padding: var(--ha-space-0);
|
||||||
}
|
}
|
||||||
::slotted([slot="footer"]) {
|
::slotted([slot="footer"]) {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ export class HaCard extends LitElement {
|
|||||||
line-height: var(--ha-line-height-expanded);
|
line-height: var(--ha-line-height-expanded);
|
||||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4);
|
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4);
|
||||||
display: block;
|
display: block;
|
||||||
margin-block-start: 0;
|
margin-block-start: var(--ha-space-0);
|
||||||
margin-block-end: 0;
|
margin-block-end: var(--ha-space-0);
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: var(--ha-font-weight-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::slotted(.card-content:not(:first-child)),
|
:host ::slotted(.card-content:not(:first-child)),
|
||||||
slot:not(:first-child)::slotted(.card-content) {
|
slot:not(:first-child)::slotted(.card-content) {
|
||||||
padding-top: 0;
|
padding-top: var(--ha-space-0);
|
||||||
margin-top: calc(var(--ha-space-2) * -1);
|
margin-top: calc(var(--ha-space-2) * -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import type { ClimateEntity } from "../data/climate";
|
import type { ClimateEntity } from "../data/climate";
|
||||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||||
import { isUnavailableState, OFF } from "../data/entity/entity";
|
import { isUnavailableState, OFF } from "../data/entity";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
@customElement("ha-climate-state")
|
@customElement("ha-climate-state")
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
|
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
|
||||||
import { html, LitElement } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||||
import type { LocalizeKeys } from "../common/translations/localize";
|
import type { LocalizeKeys } from "../common/translations/localize";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-generic-picker";
|
import "./ha-list-item";
|
||||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
import "./ha-md-divider";
|
||||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
import "./ha-select";
|
||||||
|
import type { HaSelect } from "./ha-select";
|
||||||
|
|
||||||
@customElement("ha-color-picker")
|
@customElement("ha-color-picker")
|
||||||
export class HaColorPicker extends LitElement {
|
export class HaColorPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
|
|
||||||
@property({ type: String, attribute: "default_color" })
|
@property({ type: String, attribute: "default_color" })
|
||||||
@@ -31,178 +33,137 @@ export class HaColorPicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@query("ha-select") private _select?: HaSelect;
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
|
||||||
|
this._select?.layoutOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueSelected(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (!this.isConnected) return;
|
||||||
|
const value = ev.target.value;
|
||||||
|
this.value = value === this.defaultColor ? undefined : value;
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: this.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const effectiveValue = this.value ?? this.defaultColor ?? "";
|
const value = this.value || this.defaultColor || "";
|
||||||
|
|
||||||
|
const isCustom = !(
|
||||||
|
THEME_COLORS.has(value) ||
|
||||||
|
value === "none" ||
|
||||||
|
value === "state"
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-select
|
||||||
.hass=${this.hass}
|
.icon=${Boolean(value)}
|
||||||
.disabled=${this.disabled}
|
|
||||||
.required=${this.required}
|
|
||||||
.hideClearIcon=${!this.value && !!this.defaultColor}
|
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
|
.value=${value}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.value=${effectiveValue}
|
.disabled=${this.disabled}
|
||||||
.getItems=${this._getItems}
|
@closed=${stopPropagation}
|
||||||
.rowRenderer=${this._rowRenderer}
|
@selected=${this._valueSelected}
|
||||||
.valueRenderer=${this._valueRenderer}
|
fixedMenuPosition
|
||||||
@value-changed=${this._valueChanged}
|
naturalMenuWidth
|
||||||
|
.clearable=${!this.defaultColor}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
${value
|
||||||
`;
|
? html`
|
||||||
}
|
<span slot="icon">
|
||||||
|
${value === "none"
|
||||||
private _getItems = () =>
|
? html`
|
||||||
this._getColors(
|
<ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
|
||||||
this.includeNone,
|
`
|
||||||
this.includeState,
|
: value === "state"
|
||||||
this.defaultColor,
|
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
|
||||||
this.value
|
: this._renderColorCircle(value || "grey")}
|
||||||
);
|
</span>
|
||||||
|
`
|
||||||
private _getColors = (
|
: nothing}
|
||||||
includeNone: boolean,
|
${this.includeNone
|
||||||
includeState: boolean,
|
? html`
|
||||||
defaultColor: string | undefined,
|
<ha-list-item value="none" graphic="icon">
|
||||||
currentValue: string | undefined
|
|
||||||
): PickerComboBoxItem[] => {
|
|
||||||
const items: PickerComboBoxItem[] = [];
|
|
||||||
|
|
||||||
const defaultSuffix = this.hass.localize(
|
|
||||||
"ui.components.color-picker.default"
|
|
||||||
);
|
|
||||||
|
|
||||||
const addDefaultSuffix = (label: string, isDefault: boolean) =>
|
|
||||||
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
|
|
||||||
|
|
||||||
if (includeNone) {
|
|
||||||
const noneLabel =
|
|
||||||
this.hass.localize("ui.components.color-picker.none") || "None";
|
|
||||||
items.push({
|
|
||||||
id: "none",
|
|
||||||
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
|
|
||||||
icon_path: mdiInvertColorsOff,
|
|
||||||
sorting_label: noneLabel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeState) {
|
|
||||||
const stateLabel =
|
|
||||||
this.hass.localize("ui.components.color-picker.state") || "State";
|
|
||||||
items.push({
|
|
||||||
id: "state",
|
|
||||||
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
|
|
||||||
icon_path: mdiPalette,
|
|
||||||
sorting_label: stateLabel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Array.from(THEME_COLORS).forEach((color) => {
|
|
||||||
const themeLabel =
|
|
||||||
this.hass.localize(
|
|
||||||
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
|
||||||
) || color;
|
|
||||||
items.push({
|
|
||||||
id: color,
|
|
||||||
primary: addDefaultSuffix(themeLabel, defaultColor === color),
|
|
||||||
sorting_label: themeLabel,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const isSpecial =
|
|
||||||
currentValue === "none" ||
|
|
||||||
currentValue === "state" ||
|
|
||||||
THEME_COLORS.has(currentValue || "");
|
|
||||||
|
|
||||||
const hasValue = currentValue && currentValue.length > 0;
|
|
||||||
|
|
||||||
if (hasValue && !isSpecial) {
|
|
||||||
items.push({
|
|
||||||
id: currentValue!,
|
|
||||||
primary: currentValue!,
|
|
||||||
sorting_label: currentValue!,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _rowRenderer: (
|
|
||||||
item: PickerComboBoxItem,
|
|
||||||
index?: number
|
|
||||||
) => ReturnType<typeof html> = (item) => html`
|
|
||||||
<ha-combo-box-item type="button" compact>
|
|
||||||
${item.id === "none"
|
|
||||||
? html`<ha-svg-icon
|
|
||||||
slot="start"
|
|
||||||
.path=${mdiInvertColorsOff}
|
|
||||||
></ha-svg-icon>`
|
|
||||||
: item.id === "state"
|
|
||||||
? html`<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>`
|
|
||||||
: html`<span slot="start">
|
|
||||||
${this._renderColorCircle(item.id)}
|
|
||||||
</span>`}
|
|
||||||
<span slot="headline">${item.primary}</span>
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
|
|
||||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
|
||||||
if (value === "none") {
|
|
||||||
return html`
|
|
||||||
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
|
|
||||||
<span slot="headline">
|
|
||||||
${this.hass.localize("ui.components.color-picker.none")}
|
${this.hass.localize("ui.components.color-picker.none")}
|
||||||
</span>
|
${this.defaultColor === "none"
|
||||||
`;
|
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
||||||
}
|
: nothing}
|
||||||
if (value === "state") {
|
<ha-svg-icon
|
||||||
return html`
|
slot="graphic"
|
||||||
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>
|
path=${mdiInvertColorsOff}
|
||||||
<span slot="headline">
|
></ha-svg-icon>
|
||||||
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${this.includeState
|
||||||
|
? html`
|
||||||
|
<ha-list-item value="state" graphic="icon">
|
||||||
${this.hass.localize("ui.components.color-picker.state")}
|
${this.hass.localize("ui.components.color-picker.state")}
|
||||||
</span>
|
${this.defaultColor === "state"
|
||||||
|
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
||||||
|
: nothing}
|
||||||
|
<ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon>
|
||||||
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${this.includeState || this.includeNone
|
||||||
|
? html`<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||||
|
: nothing}
|
||||||
|
${Array.from(THEME_COLORS).map(
|
||||||
|
(color) => html`
|
||||||
|
<ha-list-item .value=${color} graphic="icon">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
||||||
|
) || color}
|
||||||
|
${this.defaultColor === color
|
||||||
|
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
||||||
|
: nothing}
|
||||||
|
<span slot="graphic">${this._renderColorCircle(color)}</span>
|
||||||
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
${isCustom
|
||||||
|
? html`
|
||||||
|
<ha-list-item .value=${value} graphic="icon">
|
||||||
|
${value}
|
||||||
|
<span slot="graphic">${this._renderColorCircle(value)}</span>
|
||||||
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-select>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
|
||||||
<span slot="start">${this._renderColorCircle(value)}</span>
|
|
||||||
<span slot="headline">
|
|
||||||
${this.hass.localize(
|
|
||||||
`ui.components.color-picker.colors.${value}` as LocalizeKeys
|
|
||||||
) || value}
|
|
||||||
</span>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _renderColorCircle(color: string) {
|
private _renderColorCircle(color: string) {
|
||||||
return html`
|
return html`
|
||||||
<span
|
<span
|
||||||
|
class="circle-color"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
"--circle-color": computeCssColor(color),
|
"--circle-color": computeCssColor(color),
|
||||||
display: "block",
|
|
||||||
"background-color": "var(--circle-color, var(--divider-color))",
|
|
||||||
border: "1px solid var(--outline-color)",
|
|
||||||
"border-radius": "var(--ha-border-radius-pill)",
|
|
||||||
width: "20px",
|
|
||||||
height: "20px",
|
|
||||||
"box-sizing": "border-box",
|
|
||||||
})}
|
})}
|
||||||
></span>
|
></span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: CustomEvent<{ value?: string }>) {
|
static styles = css`
|
||||||
ev.stopPropagation();
|
.circle-color {
|
||||||
const selected = ev.detail.value;
|
display: block;
|
||||||
const normalized =
|
background-color: var(--circle-color, var(--divider-color));
|
||||||
selected && selected === this.defaultColor
|
border: 1px solid var(--outline-color);
|
||||||
? undefined
|
border-radius: var(--ha-border-radius-pill);
|
||||||
: (selected ?? undefined);
|
width: 20px;
|
||||||
this.value = normalized;
|
height: 20px;
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
ha-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -20,17 +20,6 @@ export class HaComboBoxItem extends HaMdListItem {
|
|||||||
[slot="start"] {
|
[slot="start"] {
|
||||||
--state-icon-color: var(--secondary-text-color);
|
--state-icon-color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
[slot="overline"] {
|
|
||||||
/* mimicing a floating label of mdc-select */
|
|
||||||
line-height: 1.15rem;
|
|
||||||
font-size: calc(var(--mdc-typography-subtitle1-font-size, 1rem) * 0.75);
|
|
||||||
font-weight: var(--mdc-typography-subtitle1-font-weight, 400);
|
|
||||||
font-family: var(
|
|
||||||
--mdc-typography-subtitle1-font-family,
|
|
||||||
var(--mdc-typography-font-family)
|
|
||||||
);
|
|
||||||
color: var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6));
|
|
||||||
}
|
|
||||||
[slot="headline"] {
|
[slot="headline"] {
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: var(--ha-line-height-normal);
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: var(--ha-font-size-m);
|
||||||
|
|||||||
433
src/components/ha-combo-box.ts
Normal file
433
src/components/ha-combo-box.ts
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||||
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import { comboBoxRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
|
||||||
|
import type {
|
||||||
|
ComboBoxDataProvider,
|
||||||
|
ComboBoxLight,
|
||||||
|
ComboBoxLightFilterChangedEvent,
|
||||||
|
ComboBoxLightOpenedChangedEvent,
|
||||||
|
ComboBoxLightValueChangedEvent,
|
||||||
|
} from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||||
|
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
|
||||||
|
import type { TemplateResult } from "lit";
|
||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-combo-box-item";
|
||||||
|
import "./ha-combo-box-textfield";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
import "./ha-input-helper-text";
|
||||||
|
import "./ha-textfield";
|
||||||
|
import type { HaTextField } from "./ha-textfield";
|
||||||
|
|
||||||
|
registerStyles(
|
||||||
|
"vaadin-combo-box-item",
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
:host([focused]:not([disabled])) {
|
||||||
|
background-color: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.12);
|
||||||
|
}
|
||||||
|
:host([selected]:not([disabled])) {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--mdc-theme-primary);
|
||||||
|
--mdc-ripple-color: var(--mdc-theme-primary);
|
||||||
|
--mdc-theme-text-primary-on-background: var(--mdc-theme-primary);
|
||||||
|
}
|
||||||
|
:host([selected]:not([disabled])):before {
|
||||||
|
background-color: var(--mdc-theme-primary);
|
||||||
|
opacity: 0.12;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
:host([selected][focused]:not([disabled])):before {
|
||||||
|
opacity: 0.24;
|
||||||
|
}
|
||||||
|
:host(:hover:not([disabled])) {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
[part="content"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
[part="checkmark"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
@customElement("ha-combo-box")
|
||||||
|
export class HaComboBox extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public validationMessage?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public invalid = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public icon = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public items?: any[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public filteredItems?: any[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public dataProvider?: ComboBoxDataProvider<any>;
|
||||||
|
|
||||||
|
@property({ attribute: "allow-custom-value", type: Boolean })
|
||||||
|
public allowCustomValue = false;
|
||||||
|
|
||||||
|
@property({ attribute: "item-value-path" }) public itemValuePath = "value";
|
||||||
|
|
||||||
|
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
|
||||||
|
|
||||||
|
@property({ attribute: "item-id-path" }) public itemIdPath?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public renderer?: ComboBoxLitRenderer<any>;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public opened = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "hide-clear-icon" })
|
||||||
|
public hideClearIcon = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "clear-initial-value" })
|
||||||
|
public clearInitialValue = false;
|
||||||
|
|
||||||
|
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
|
||||||
|
|
||||||
|
@query("ha-combo-box-textfield", true) private _inputElement!: HaTextField;
|
||||||
|
|
||||||
|
@state({ type: Boolean }) private _forceBlankValue = false;
|
||||||
|
|
||||||
|
private _overlayMutationObserver?: MutationObserver;
|
||||||
|
|
||||||
|
private _bodyMutationObserver?: MutationObserver;
|
||||||
|
|
||||||
|
public async open() {
|
||||||
|
await this.updateComplete;
|
||||||
|
this._comboBox?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async focus() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this._inputElement?.updateComplete;
|
||||||
|
this._inputElement?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
if (this._overlayMutationObserver) {
|
||||||
|
this._overlayMutationObserver.disconnect();
|
||||||
|
this._overlayMutationObserver = undefined;
|
||||||
|
}
|
||||||
|
if (this._bodyMutationObserver) {
|
||||||
|
this._bodyMutationObserver.disconnect();
|
||||||
|
this._bodyMutationObserver = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get selectedItem() {
|
||||||
|
return this._comboBox.selectedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setInputValue(value: string) {
|
||||||
|
this._comboBox.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTextFieldValue(value: string) {
|
||||||
|
this._inputElement.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<!-- @ts-ignore Tag definition is not included in theme folder -->
|
||||||
|
<vaadin-combo-box-light
|
||||||
|
.itemValuePath=${this.itemValuePath}
|
||||||
|
.itemIdPath=${this.itemIdPath}
|
||||||
|
.itemLabelPath=${this.itemLabelPath}
|
||||||
|
.items=${this.items}
|
||||||
|
.value=${this.value || ""}
|
||||||
|
.filteredItems=${this.filteredItems}
|
||||||
|
.dataProvider=${this.dataProvider}
|
||||||
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
${comboBoxRenderer(this.renderer || this._defaultRowRenderer)}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
attr-for-value="value"
|
||||||
|
>
|
||||||
|
<ha-combo-box-textfield
|
||||||
|
label=${ifDefined(this.label)}
|
||||||
|
placeholder=${ifDefined(this.placeholder)}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
?required=${this.required}
|
||||||
|
validationMessage=${ifDefined(this.validationMessage)}
|
||||||
|
.errorMessage=${this.errorMessage}
|
||||||
|
class="input"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="off"
|
||||||
|
.autocorrect=${false}
|
||||||
|
input-spellcheck="false"
|
||||||
|
.suffix=${html`<div
|
||||||
|
style="width: 28px;"
|
||||||
|
role="none presentation"
|
||||||
|
></div>`}
|
||||||
|
.icon=${this.icon}
|
||||||
|
.invalid=${this.invalid}
|
||||||
|
.forceBlankValue=${this._forceBlankValue}
|
||||||
|
>
|
||||||
|
<slot name="icon" slot="leadingIcon"></slot>
|
||||||
|
</ha-combo-box-textfield>
|
||||||
|
${this.value && !this.hideClearIcon
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label=${ifDefined(this.hass?.localize("ui.common.clear"))}
|
||||||
|
class=${`clear-button ${this.label ? "" : "no-label"}`}
|
||||||
|
.path=${mdiClose}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
@click=${this._clearValue}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: ""}
|
||||||
|
<ha-svg-icon
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label=${ifDefined(this.label)}
|
||||||
|
aria-expanded=${this.opened ? "true" : "false"}
|
||||||
|
class=${`toggle-button ${this.label ? "" : "no-label"}`}
|
||||||
|
.path=${this.opened ? mdiMenuUp : mdiMenuDown}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
@click=${this._toggleOpen}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</vaadin-combo-box-light>
|
||||||
|
${this._renderHelper()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderHelper() {
|
||||||
|
return this.helper
|
||||||
|
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||||
|
>${this.helper}</ha-input-helper-text
|
||||||
|
>`
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _defaultRowRenderer: ComboBoxLitRenderer<
|
||||||
|
string | Record<string, any>
|
||||||
|
> = (item) => html`
|
||||||
|
<ha-combo-box-item type="button">
|
||||||
|
${this.itemLabelPath ? item[this.itemLabelPath] : item}
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
|
||||||
|
private _clearValue(ev: Event) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
fireEvent(this, "value-changed", { value: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleOpen(ev: Event) {
|
||||||
|
if (this.opened) {
|
||||||
|
this._comboBox?.close();
|
||||||
|
ev.stopPropagation();
|
||||||
|
} else {
|
||||||
|
this._comboBox?.inputElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const opened = ev.detail.value;
|
||||||
|
// delay this so we can handle click event for toggle button before setting _opened
|
||||||
|
setTimeout(() => {
|
||||||
|
this.opened = opened;
|
||||||
|
fireEvent(this, "opened-changed", { value: ev.detail.value });
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (this.clearInitialValue) {
|
||||||
|
this.setTextFieldValue("");
|
||||||
|
if (opened) {
|
||||||
|
// Wait 100ms to be sure vaddin-combo-box-light already tried to set the value
|
||||||
|
setTimeout(() => {
|
||||||
|
this._forceBlankValue = false;
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
this._forceBlankValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opened) {
|
||||||
|
const overlay = document.querySelector<HTMLElement>(
|
||||||
|
"vaadin-combo-box-overlay"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (overlay) {
|
||||||
|
this._removeInert(overlay);
|
||||||
|
}
|
||||||
|
this._observeBody();
|
||||||
|
} else {
|
||||||
|
this._bodyMutationObserver?.disconnect();
|
||||||
|
this._bodyMutationObserver = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _observeBody() {
|
||||||
|
if ("MutationObserver" in window && !this._bodyMutationObserver) {
|
||||||
|
this._bodyMutationObserver = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
mutation.addedNodes.forEach((node) => {
|
||||||
|
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
|
||||||
|
this._removeInert(node as HTMLElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mutation.removedNodes.forEach((node) => {
|
||||||
|
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
|
||||||
|
this._overlayMutationObserver?.disconnect();
|
||||||
|
this._overlayMutationObserver = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this._bodyMutationObserver.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeInert(overlay: HTMLElement) {
|
||||||
|
if (overlay.inert) {
|
||||||
|
overlay.inert = false;
|
||||||
|
this._overlayMutationObserver?.disconnect();
|
||||||
|
this._overlayMutationObserver = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("MutationObserver" in window && !this._overlayMutationObserver) {
|
||||||
|
this._overlayMutationObserver = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.attributeName === "inert") {
|
||||||
|
const target = mutation.target as HTMLElement;
|
||||||
|
if (target.inert) {
|
||||||
|
this._overlayMutationObserver?.disconnect();
|
||||||
|
this._overlayMutationObserver = undefined;
|
||||||
|
target.inert = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this._overlayMutationObserver.observe(overlay, {
|
||||||
|
attributes: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterChanged(ev: ComboBoxLightFilterChangedEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
fireEvent(this, "filter-changed", { value: ev.detail.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (!this.allowCustomValue) {
|
||||||
|
// @ts-ignore
|
||||||
|
this._comboBox._closeOnBlurIsPrevented = true;
|
||||||
|
}
|
||||||
|
if (!this.opened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
if (newValue !== this.value) {
|
||||||
|
fireEvent(this, "value-changed", { value: newValue || undefined });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
vaadin-combo-box-light {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
ha-combo-box-textfield {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
ha-combo-box-textfield > ha-icon-button {
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
padding: 2px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
ha-svg-icon {
|
||||||
|
color: var(--input-dropdown-icon-color);
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.toggle-button {
|
||||||
|
right: 12px;
|
||||||
|
top: -10px;
|
||||||
|
inset-inline-start: initial;
|
||||||
|
inset-inline-end: 12px;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
:host([opened]) .toggle-button {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.toggle-button[disabled],
|
||||||
|
.clear-button[disabled] {
|
||||||
|
color: var(--disabled-text-color);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.toggle-button.no-label {
|
||||||
|
top: -3px;
|
||||||
|
}
|
||||||
|
.clear-button {
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
top: -7px;
|
||||||
|
right: 36px;
|
||||||
|
inset-inline-start: initial;
|
||||||
|
inset-inline-end: 36px;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.clear-button.no-label {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
ha-input-helper-text {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-combo-box": HaComboBox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"filter-changed": { value: string };
|
||||||
|
"opened-changed": { value: boolean };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,20 @@
|
|||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import type { ConfigEntry } from "../data/config_entries";
|
import type { ConfigEntry } from "../data/config_entries";
|
||||||
import { getConfigEntries } from "../data/config_entries";
|
import { getConfigEntries } from "../data/config_entries";
|
||||||
import { domainToName } from "../data/integration";
|
import { domainToName } from "../data/integration";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { ValueChangedEvent, HomeAssistant } from "../types";
|
||||||
|
import { brandsUrl } from "../util/brands-url";
|
||||||
|
import "./ha-combo-box";
|
||||||
|
import type { HaComboBox } from "./ha-combo-box";
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
import "./ha-domain-icon";
|
|
||||||
import "./ha-generic-picker";
|
|
||||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
|
||||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
|
||||||
|
|
||||||
const SEARCH_KEYS = [
|
export interface ConfigEntryExtended extends ConfigEntry {
|
||||||
{ name: "primary", weight: 10 },
|
localized_domain_name?: string;
|
||||||
{ name: "secondary", weight: 8 },
|
}
|
||||||
{ name: "icon", weight: 5 },
|
|
||||||
];
|
|
||||||
|
|
||||||
@customElement("ha-config-entry-picker")
|
@customElement("ha-config-entry-picker")
|
||||||
class HaConfigEntryPicker extends LitElement {
|
class HaConfigEntryPicker extends LitElement {
|
||||||
@@ -30,106 +28,119 @@ class HaConfigEntryPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@state() private _configEntries?: PickerComboBoxItem[];
|
@state() private _configEntries?: ConfigEntryExtended[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@query("ha-generic-picker") private _picker!: HaGenericPicker;
|
@query("ha-combo-box") private _comboBox!: HaComboBox;
|
||||||
|
|
||||||
public open() {
|
public open() {
|
||||||
this._picker?.open();
|
this._comboBox?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
this._picker?.focus();
|
this._comboBox?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
this._getConfigEntries();
|
this._getConfigEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (
|
||||||
|
item
|
||||||
|
) => html`
|
||||||
|
<ha-combo-box-item type="button">
|
||||||
|
<span slot="headline">
|
||||||
|
${item.title ||
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.unnamed_entry"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="supporting-text">${item.localized_domain_name}</span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
slot="start"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: item.domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
@error=${this._onImageError}
|
||||||
|
@load=${this._onImageLoad}
|
||||||
|
/>
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._configEntries) {
|
if (!this._configEntries) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${this.label === undefined && this.hass
|
.label=${this.label === undefined && this.hass
|
||||||
? this.hass.localize("ui.components.config-entry-picker.config_entry")
|
? this.hass.localize("ui.components.config-entry-picker.config_entry")
|
||||||
: this.label}
|
: this.label}
|
||||||
.value=${this.value}
|
.value=${this._value}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.renderer=${this._rowRenderer}
|
||||||
.getItems=${this._getItems}
|
.items=${this._configEntries}
|
||||||
.searchKeys=${SEARCH_KEYS}
|
item-value-path="entry_id"
|
||||||
.valueRenderer=${this._valueRenderer}
|
item-id-path="entry_id"
|
||||||
|
item-label-path="title"
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></ha-generic-picker>
|
></ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
private _onImageLoad(ev) {
|
||||||
<ha-combo-box-item type="button">
|
ev.target.style.visibility = "initial";
|
||||||
<span slot="headline">${item.primary}</span>
|
}
|
||||||
<span slot="supporting-text">${item.secondary}</span>
|
|
||||||
<ha-domain-icon
|
private _onImageError(ev) {
|
||||||
slot="start"
|
ev.target.style.visibility = "hidden";
|
||||||
.hass=${this.hass}
|
}
|
||||||
.domain=${item.icon!}
|
|
||||||
brand-fallback
|
|
||||||
></ha-domain-icon>
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
|
|
||||||
private async _getConfigEntries() {
|
private async _getConfigEntries() {
|
||||||
getConfigEntries(this.hass, {
|
getConfigEntries(this.hass, {
|
||||||
type: ["device", "hub", "service"],
|
type: ["device", "hub", "service"],
|
||||||
domain: this.integration,
|
domain: this.integration,
|
||||||
}).then((configEntries) => {
|
}).then((configEntries) => {
|
||||||
this._configEntries = configEntries.map((entry: ConfigEntry) => {
|
this._configEntries = configEntries
|
||||||
const domainName = domainToName(this.hass.localize, entry.domain);
|
.map(
|
||||||
return {
|
(entry: ConfigEntry): ConfigEntryExtended => ({
|
||||||
id: entry.entry_id,
|
...entry,
|
||||||
icon: entry.domain,
|
localized_domain_name: domainToName(
|
||||||
primary:
|
this.hass.localize,
|
||||||
entry.title ||
|
entry.domain
|
||||||
this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_entry.unnamed_entry"
|
|
||||||
),
|
),
|
||||||
secondary: domainName,
|
})
|
||||||
sorting_label: [entry.title, domainName].filter(Boolean).join("_"),
|
)
|
||||||
};
|
.sort((conf1, conf2) =>
|
||||||
});
|
caseInsensitiveStringCompare(
|
||||||
|
conf1.localized_domain_name + conf1.title,
|
||||||
|
conf2.localized_domain_name + conf2.title,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueRenderer = (itemId: string) => {
|
private get _value() {
|
||||||
const item = this._configEntries!.find((entry) => entry.id === itemId);
|
return this.value || "";
|
||||||
return html`<span
|
}
|
||||||
style="display: flex; align-items: center; gap: var(--ha-space-2);"
|
|
||||||
slot="headline"
|
|
||||||
>${item?.icon
|
|
||||||
? html`<ha-domain-icon
|
|
||||||
.hass=${this.hass}
|
|
||||||
.domain=${item.icon!}
|
|
||||||
brand-fallback
|
|
||||||
></ha-domain-icon>`
|
|
||||||
: nothing}${item?.primary || "Unknown"}</span
|
|
||||||
>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _getItems = () => this._configEntries!;
|
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const newValue = ev.detail.value;
|
const newValue = ev.detail.value;
|
||||||
|
|
||||||
if (newValue !== this.value) {
|
if (newValue !== this._value) {
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import type { ConfigEntry, SubEntry } from "../data/config_entries";
|
|||||||
import { getConfigEntry, getSubEntries } from "../data/config_entries";
|
import { getConfigEntry, getSubEntries } from "../data/config_entries";
|
||||||
import type { Agent } from "../data/conversation";
|
import type { Agent } from "../data/conversation";
|
||||||
import { listAgents } from "../data/conversation";
|
import { listAgents } from "../data/conversation";
|
||||||
import { getExtendedEntityRegistryEntry } from "../data/entity/entity_registry";
|
|
||||||
import { fetchIntegrationManifest } from "../data/integration";
|
import { fetchIntegrationManifest } from "../data/integration";
|
||||||
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
|
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
|
||||||
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
import "./ha-select";
|
import "./ha-select";
|
||||||
import type { HaSelect } from "./ha-select";
|
import type { HaSelect } from "./ha-select";
|
||||||
|
import { getExtendedEntityRegistryEntry } from "../data/entity_registry";
|
||||||
|
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||||
|
|
||||||
const NONE = "__NONE_OPTION__";
|
const NONE = "__NONE_OPTION__";
|
||||||
|
|
||||||
|
|||||||
@@ -101,13 +101,14 @@ export class HaDialog extends DialogBase {
|
|||||||
}
|
}
|
||||||
.mdc-dialog__container {
|
.mdc-dialog__container {
|
||||||
align-items: var(--vertical-align-dialog, center);
|
align-items: var(--vertical-align-dialog, center);
|
||||||
padding: var(--dialog-container-padding, 0);
|
padding: var(--dialog-container-padding, var(--ha-space-0));
|
||||||
}
|
}
|
||||||
.mdc-dialog__title {
|
.mdc-dialog__title {
|
||||||
padding: var(--ha-space-4) var(--ha-space-4) 0 var(--ha-space-4);
|
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-0)
|
||||||
|
var(--ha-space-4);
|
||||||
}
|
}
|
||||||
.mdc-dialog__title:has(span) {
|
.mdc-dialog__title:has(span) {
|
||||||
padding: var(--ha-space-3) var(--ha-space-3) 0;
|
padding: var(--ha-space-3) var(--ha-space-3) var(--ha-space-0);
|
||||||
}
|
}
|
||||||
.mdc-dialog__title::before {
|
.mdc-dialog__title::before {
|
||||||
content: unset;
|
content: unset;
|
||||||
@@ -135,7 +136,7 @@ export class HaDialog extends DialogBase {
|
|||||||
--ha-dialog-surface-background,
|
--ha-dialog-surface-background,
|
||||||
var(--mdc-theme-surface, #fff)
|
var(--mdc-theme-surface, #fff)
|
||||||
);
|
);
|
||||||
padding: var(--dialog-surface-padding, 0);
|
padding: var(--dialog-surface-padding, var(--ha-space-0));
|
||||||
}
|
}
|
||||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -167,7 +168,7 @@ export class HaDialog extends DialogBase {
|
|||||||
}
|
}
|
||||||
.dialog-actions {
|
.dialog-actions {
|
||||||
inset-inline-start: initial !important;
|
inset-inline-start: initial !important;
|
||||||
inset-inline-end: 0 !important;
|
inset-inline-end: var(--ha-space-0) !important;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export class HaDomainIcon extends LitElement {
|
|||||||
if (icn) {
|
if (icn) {
|
||||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._renderFallback();
|
return this._renderFallback();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
|
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
|
||||||
import "@home-assistant/webawesome/dist/components/icon/icon";
|
import { css, type CSSResultGroup } from "lit";
|
||||||
import { css, type CSSResultGroup, html } from "lit";
|
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import "./ha-svg-icon";
|
|
||||||
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Home Assistant dropdown item component
|
* Home Assistant dropdown item component
|
||||||
@@ -17,16 +14,6 @@ import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
|
|||||||
*/
|
*/
|
||||||
@customElement("ha-dropdown-item")
|
@customElement("ha-dropdown-item")
|
||||||
export class HaDropdownItem extends DropdownItem {
|
export class HaDropdownItem extends DropdownItem {
|
||||||
protected renderCheckboxIcon() {
|
|
||||||
return html`
|
|
||||||
<ha-svg-icon
|
|
||||||
id="check"
|
|
||||||
part="checkmark"
|
|
||||||
.path=${this.checked ? mdiCheckboxMarked : mdiCheckboxBlankOutline}
|
|
||||||
></ha-svg-icon>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
DropdownItem.styles,
|
DropdownItem.styles,
|
||||||
@@ -35,10 +22,6 @@ export class HaDropdownItem extends DropdownItem {
|
|||||||
min-height: var(--ha-space-10);
|
min-height: var(--ha-space-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#check {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
#icon ::slotted(*) {
|
#icon ::slotted(*) {
|
||||||
color: var(--ha-color-on-neutral-normal);
|
color: var(--ha-color-on-neutral-normal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { computeCssColor } from "../common/color/compute-color";
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { navigate } from "../common/navigate";
|
import { navigate } from "../common/navigate";
|
||||||
import { stringCompare } from "../common/string/compare";
|
import { stringCompare } from "../common/string/compare";
|
||||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
import type { LabelRegistryEntry } from "../data/label_registry";
|
||||||
import { subscribeLabelRegistry } from "../data/label/label_registry";
|
import { subscribeLabelRegistry } from "../data/label_registry";
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
|
||||||
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||||
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { LitElement, html } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
@@ -12,9 +12,9 @@ import { updateAreaRegistryEntry } from "../data/area_registry";
|
|||||||
import type {
|
import type {
|
||||||
DeviceEntityDisplayLookup,
|
DeviceEntityDisplayLookup,
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
} from "../data/device/device_registry";
|
} from "../data/device_registry";
|
||||||
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
|
import { getDeviceEntityDisplayLookup } from "../data/device_registry";
|
||||||
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
|
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
createFloorRegistryEntry,
|
createFloorRegistryEntry,
|
||||||
getFloorAreaLookup,
|
getFloorAreaLookup,
|
||||||
@@ -35,12 +35,6 @@ import "./ha-svg-icon";
|
|||||||
|
|
||||||
const ADD_NEW_ID = "___ADD_NEW___";
|
const ADD_NEW_ID = "___ADD_NEW___";
|
||||||
|
|
||||||
const SEARCH_KEYS = [
|
|
||||||
{ name: "search_labels.floorName", weight: 10 },
|
|
||||||
{ name: "search_labels.aliases", weight: 8 },
|
|
||||||
{ name: "search_labels.floor_id", weight: 3 },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface FloorComboBoxItem extends PickerComboBoxItem {
|
interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||||
floor?: FloorRegistryEntry;
|
floor?: FloorRegistryEntry;
|
||||||
}
|
}
|
||||||
@@ -291,11 +285,10 @@ export class HaFloorPicker extends LitElement {
|
|||||||
id: floor.floor_id,
|
id: floor.floor_id,
|
||||||
primary: floorName,
|
primary: floorName,
|
||||||
floor: floor,
|
floor: floor,
|
||||||
search_labels: {
|
sorting_label: floor.level?.toString() || "zzzzz",
|
||||||
floorName,
|
search_labels: [floorName, floor.floor_id, ...floor.aliases].filter(
|
||||||
floor_id: floor.floor_id,
|
(v): v is string => Boolean(v)
|
||||||
aliases: floor.aliases.join(" "),
|
),
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -303,7 +296,7 @@ export class HaFloorPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _rowRenderer: RenderItemFunction<FloorComboBoxItem> = (item) => html`
|
private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (item) => html`
|
||||||
<ha-combo-box-item type="button" compact>
|
<ha-combo-box-item type="button" compact>
|
||||||
${item.icon_path
|
${item.icon_path
|
||||||
? html`
|
? html`
|
||||||
@@ -389,23 +382,17 @@ export class HaFloorPicker extends LitElement {
|
|||||||
<ha-generic-picker
|
<ha-generic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.disabled=${this.disabled}
|
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
|
||||||
.placeholder=${placeholder}
|
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
.emptyLabel=${this.hass.localize(
|
.emptyLabel=${this.hass.localize(
|
||||||
"ui.components.floor-picker.no_floors"
|
"ui.components.floor-picker.no_floors"
|
||||||
)}
|
)}
|
||||||
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
.searchKeys=${SEARCH_KEYS}
|
|
||||||
.unknownItemText=${this.hass.localize(
|
|
||||||
"ui.components.floor-picker.unknown"
|
|
||||||
)}
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -4,13 +4,9 @@ import { mdiPlaylistPlus } from "@mdi/js";
|
|||||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { tinykeys } from "tinykeys";
|
import { tinykeys } from "tinykeys";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { PickerMixin } from "../mixins/picker-mixin";
|
|
||||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import { isIosApp } from "../util/is_ios";
|
|
||||||
import "./ha-bottom-sheet";
|
import "./ha-bottom-sheet";
|
||||||
import "./ha-button";
|
import "./ha-button";
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
@@ -22,21 +18,36 @@ import type {
|
|||||||
PickerComboBoxSearchFn,
|
PickerComboBoxSearchFn,
|
||||||
} from "./ha-picker-combo-box";
|
} from "./ha-picker-combo-box";
|
||||||
import "./ha-picker-field";
|
import "./ha-picker-field";
|
||||||
|
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
@customElement("ha-generic-picker")
|
@customElement("ha-generic-picker")
|
||||||
export class HaGenericPicker extends PickerMixin(LitElement) {
|
export class HaGenericPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "allow-custom-value" })
|
@property({ type: Boolean, attribute: "allow-custom-value" })
|
||||||
public allowCustomValue;
|
public allowCustomValue;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
@property({ type: String, attribute: "search-label" })
|
@property({ type: String, attribute: "search-label" })
|
||||||
public searchLabel?: string;
|
public searchLabel?: string;
|
||||||
|
|
||||||
/** To prevent lags, getItems needs to be memoized */
|
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||||
|
public hideClearIcon = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public getItems!: (
|
public getItems?: (
|
||||||
searchString?: string,
|
searchString?: string,
|
||||||
section?: string
|
section?: string
|
||||||
) => (PickerComboBoxItem | string)[];
|
) => (PickerComboBoxItem | string)[];
|
||||||
@@ -48,10 +59,10 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
|||||||
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
|
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
public valueRenderer?: PickerValueRenderer;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public searchKeys?: FuseWeightedKey[];
|
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public notFoundLabel?: string | ((search: string) => string);
|
public notFoundLabel?: string | ((search: string) => string);
|
||||||
@@ -96,12 +107,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
|||||||
|
|
||||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "use-top-label" })
|
|
||||||
public useTopLabel = false;
|
|
||||||
|
|
||||||
@property({ attribute: "custom-value-label" })
|
|
||||||
public customValueLabel?: string;
|
|
||||||
|
|
||||||
@query(".container") private _containerElement?: HTMLDivElement;
|
@query(".container") private _containerElement?: HTMLDivElement;
|
||||||
|
|
||||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||||
@@ -124,20 +129,14 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
|||||||
// helper to set new value after closing picker, to avoid flicker
|
// helper to set new value after closing picker, to avoid flicker
|
||||||
private _newValue?: string;
|
private _newValue?: string;
|
||||||
|
|
||||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public invalid = false;
|
|
||||||
|
|
||||||
private _unsubscribeTinyKeys?: () => void;
|
private _unsubscribeTinyKeys?: () => void;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
// Only show label if it's not a top label and there is a value.
|
return html`
|
||||||
const label = this.useTopLabel && this.value ? undefined : this.label;
|
${this.label
|
||||||
|
|
||||||
return html`<div class="container">
|
|
||||||
${this.useTopLabel && this.label
|
|
||||||
? html`<label ?disabled=${this.disabled}>${this.label}</label>`
|
? html`<label ?disabled=${this.disabled}>${this.label}</label>`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
<div class="container">
|
||||||
<div id="picker">
|
<div id="picker">
|
||||||
<slot name="field">
|
<slot name="field">
|
||||||
${this.addButtonLabel && !this.value
|
${this.addButtonLabel && !this.value
|
||||||
@@ -157,28 +156,16 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
|||||||
type="button"
|
type="button"
|
||||||
class=${this._opened ? "opened" : ""}
|
class=${this._opened ? "opened" : ""}
|
||||||
compact
|
compact
|
||||||
.unknown=${this._unknownValue(
|
|
||||||
this.allowCustomValue,
|
|
||||||
this.value,
|
|
||||||
this.getItems()
|
|
||||||
)}
|
|
||||||
.unknownItemText=${this.unknownItemText}
|
|
||||||
aria-label=${ifDefined(this.label)}
|
aria-label=${ifDefined(this.label)}
|
||||||
@click=${this.open}
|
@click=${this.open}
|
||||||
@clear=${this._clear}
|
@clear=${this._clear}
|
||||||
.icon=${this.icon}
|
|
||||||
.image=${this.image}
|
|
||||||
.label=${label}
|
|
||||||
.placeholder=${this.placeholder}
|
.placeholder=${this.placeholder}
|
||||||
.helper=${this.helper}
|
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.valueRenderer=${this.valueRenderer}
|
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.invalid=${this.invalid}
|
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
|
.valueRenderer=${this.valueRenderer}
|
||||||
>
|
>
|
||||||
<slot name="start"></slot>
|
|
||||||
</ha-picker-field>`}
|
</ha-picker-field>`}
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,7 +204,8 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
|||||||
</ha-bottom-sheet>`
|
</ha-bottom-sheet>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._renderHelper()}`;
|
${this._renderHelper()}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderComboBox(dialogMode = false) {
|
private _renderComboBox(dialogMode = false) {
|
||||||
@@ -226,7 +214,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<ha-picker-combo-box
|
<ha-picker-combo-box
|
||||||
id="combo-box"
|
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
.label=${this.searchLabel}
|
.label=${this.searchLabel}
|
||||||
@@ -242,59 +229,21 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
|||||||
.sections=${this.sections}
|
.sections=${this.sections}
|
||||||
.sectionTitleFunction=${this.sectionTitleFunction}
|
.sectionTitleFunction=${this.sectionTitleFunction}
|
||||||
.selectedSection=${this.selectedSection}
|
.selectedSection=${this.selectedSection}
|
||||||
.searchKeys=${this.searchKeys}
|
|
||||||
.customValueLabel=${this.customValueLabel}
|
|
||||||
></ha-picker-combo-box>
|
></ha-picker-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _unknownValue = memoizeOne(
|
|
||||||
(
|
|
||||||
allowCustomValue: boolean,
|
|
||||||
value?: string,
|
|
||||||
items?: (PickerComboBoxItem | string)[]
|
|
||||||
) => {
|
|
||||||
if (
|
|
||||||
allowCustomValue ||
|
|
||||||
value === undefined ||
|
|
||||||
value === null ||
|
|
||||||
value === "" ||
|
|
||||||
!items
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !items.some(
|
|
||||||
(item) => typeof item !== "string" && item.id === value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _renderHelper() {
|
private _renderHelper() {
|
||||||
const showError = this.invalid && this.errorMessage;
|
return this.helper
|
||||||
const showHelper = !showError && this.helper;
|
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||||
|
>${this.helper}</ha-input-helper-text
|
||||||
if (!showError && !showHelper) {
|
>`
|
||||||
return nothing;
|
: nothing;
|
||||||
}
|
|
||||||
|
|
||||||
return html`<ha-input-helper-text .disabled=${this.disabled}>
|
|
||||||
${showError ? this.errorMessage : this.helper}
|
|
||||||
</ha-input-helper-text>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dialogOpened = () => {
|
private _dialogOpened = () => {
|
||||||
this._opened = true;
|
this._opened = true;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (this.hass && isIosApp(this.hass)) {
|
|
||||||
this.hass.auth.external!.fireMessage({
|
|
||||||
type: "focus_element",
|
|
||||||
payload: {
|
|
||||||
element_id: "combo-box",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._comboBox?.focus();
|
this._comboBox?.focus();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -321,7 +270,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
|||||||
this._newValue = value;
|
this._newValue = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clear(e: CustomEvent) {
|
private _clear(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this._setValue(undefined);
|
this._setValue(undefined);
|
||||||
}
|
}
|
||||||
@@ -388,20 +337,14 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
|||||||
display: block;
|
display: block;
|
||||||
margin: var(--ha-space-2) 0 0;
|
margin: var(--ha-space-2) 0 0;
|
||||||
}
|
}
|
||||||
:host([invalid]) ha-input-helper-text {
|
|
||||||
color: var(--mdc-theme-error, var(--error-color, #b00020));
|
|
||||||
}
|
|
||||||
|
|
||||||
wa-popover {
|
wa-popover {
|
||||||
--wa-space-l: 0;
|
--wa-space-l: var(--ha-space-0);
|
||||||
}
|
}
|
||||||
|
|
||||||
wa-popover::part(body) {
|
wa-popover::part(body) {
|
||||||
width: max(var(--body-width), 250px);
|
width: max(var(--body-width), 250px);
|
||||||
max-width: var(
|
max-width: max(var(--body-width), 250px);
|
||||||
--ha-generic-picker-max-width,
|
|
||||||
max(var(--body-width), 250px)
|
|
||||||
);
|
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
height: 70vh;
|
height: 70vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -413,12 +356,18 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-height: 1000px) {
|
||||||
|
wa-popover::part(body) {
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ha-bottom-sheet {
|
ha-bottom-sheet {
|
||||||
--ha-bottom-sheet-height: 90vh;
|
--ha-bottom-sheet-height: 90vh;
|
||||||
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
|
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
|
||||||
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
|
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
|
||||||
--ha-bottom-sheet-max-width: 600px;
|
--ha-bottom-sheet-max-width: 600px;
|
||||||
--ha-bottom-sheet-padding: 0;
|
--ha-bottom-sheet-padding: var(--ha-space-0);
|
||||||
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
||||||
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
|
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { isUnavailableState, OFF } from "../data/entity/entity";
|
import { isUnavailableState, OFF } from "../data/entity";
|
||||||
import type { HumidifierEntity } from "../data/humidifier";
|
import type { HumidifierEntity } from "../data/humidifier";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import type {
|
||||||
|
ComboBoxDataProviderCallback,
|
||||||
|
ComboBoxDataProviderParams,
|
||||||
|
} from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { LitElement, css, html } from "lit";
|
import { LitElement, css, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
@@ -6,54 +10,35 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { customIcons } from "../data/custom_icons";
|
import { customIcons } from "../data/custom_icons";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box";
|
||||||
import "./ha-generic-picker";
|
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
import "./ha-combo-box-item";
|
||||||
|
|
||||||
|
interface IconItem {
|
||||||
|
icon: string;
|
||||||
|
parts: Set<string>;
|
||||||
|
keywords: string[];
|
||||||
|
}
|
||||||
|
|
||||||
interface RankedIcon {
|
interface RankedIcon {
|
||||||
item: PickerComboBoxItem;
|
icon: string;
|
||||||
rank: number;
|
rank: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ICONS: PickerComboBoxItem[] = [];
|
let ICONS: IconItem[] = [];
|
||||||
let ICONS_LOADED = false;
|
let ICONS_LOADED = false;
|
||||||
|
|
||||||
interface IconData {
|
|
||||||
name: string;
|
|
||||||
keywords?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const createIconItem = (icon: IconData, prefix: string): PickerComboBoxItem => {
|
|
||||||
const iconId = `${prefix}:${icon.name}`;
|
|
||||||
const iconName = icon.name;
|
|
||||||
const parts = iconName.split("-");
|
|
||||||
const keywords = icon.keywords ?? [];
|
|
||||||
const searchLabels: Record<string, string> = {
|
|
||||||
iconName,
|
|
||||||
};
|
|
||||||
parts.forEach((part, index) => {
|
|
||||||
searchLabels[`part${index}`] = part;
|
|
||||||
});
|
|
||||||
keywords.forEach((keyword, index) => {
|
|
||||||
searchLabels[`keyword${index}`] = keyword;
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
id: iconId,
|
|
||||||
primary: iconId,
|
|
||||||
icon: iconId,
|
|
||||||
search_labels: searchLabels,
|
|
||||||
sorting_label: iconId,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadIcons = async () => {
|
const loadIcons = async () => {
|
||||||
ICONS_LOADED = true;
|
ICONS_LOADED = true;
|
||||||
|
|
||||||
const iconList = await import("../../build/mdi/iconList.json");
|
const iconList = await import("../../build/mdi/iconList.json");
|
||||||
ICONS = iconList.default.map((icon) => createIconItem(icon, "mdi"));
|
ICONS = iconList.default.map((icon) => ({
|
||||||
|
icon: `mdi:${icon.name}`,
|
||||||
|
parts: new Set(icon.name.split("-")),
|
||||||
|
keywords: icon.keywords,
|
||||||
|
}));
|
||||||
|
|
||||||
const customIconLoads: Promise<PickerComboBoxItem[]>[] = [];
|
const customIconLoads: Promise<IconItem[]>[] = [];
|
||||||
Object.keys(customIcons).forEach((iconSet) => {
|
Object.keys(customIcons).forEach((iconSet) => {
|
||||||
customIconLoads.push(loadCustomIconItems(iconSet));
|
customIconLoads.push(loadCustomIconItems(iconSet));
|
||||||
});
|
});
|
||||||
@@ -62,16 +47,19 @@ const loadIcons = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadCustomIconItems = async (
|
const loadCustomIconItems = async (iconsetPrefix: string) => {
|
||||||
iconsetPrefix: string
|
|
||||||
): Promise<PickerComboBoxItem[]> => {
|
|
||||||
try {
|
try {
|
||||||
const getIconList = customIcons[iconsetPrefix].getIconList;
|
const getIconList = customIcons[iconsetPrefix].getIconList;
|
||||||
if (typeof getIconList !== "function") {
|
if (typeof getIconList !== "function") {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const iconList = await getIconList();
|
const iconList = await getIconList();
|
||||||
return iconList.map((icon) => createIconItem(icon, iconsetPrefix));
|
const customIconItems = iconList.map((icon) => ({
|
||||||
|
icon: `${iconsetPrefix}:${icon.name}`,
|
||||||
|
parts: new Set(icon.name.split("-")),
|
||||||
|
keywords: icon.keywords ?? [],
|
||||||
|
}));
|
||||||
|
return customIconItems;
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(`Unable to load icon list for ${iconsetPrefix} iconset`);
|
console.warn(`Unable to load icon list for ${iconsetPrefix} iconset`);
|
||||||
@@ -79,10 +67,10 @@ const loadCustomIconItems = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => html`
|
||||||
<ha-combo-box-item type="button">
|
<ha-combo-box-item type="button">
|
||||||
<ha-icon .icon=${item.id} slot="start"></ha-icon>
|
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
|
||||||
${item.id}
|
${item.icon}
|
||||||
</ha-combo-box-item>
|
</ha-combo-box-item>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -106,99 +94,85 @@ export class HaIconPicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public invalid = false;
|
@property({ type: Boolean }) public invalid = false;
|
||||||
|
|
||||||
private _getIconPickerItems = (): PickerComboBoxItem[] => ICONS;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
item-value-path="icon"
|
||||||
|
item-label-path="icon"
|
||||||
|
.value=${this._value}
|
||||||
allow-custom-value
|
allow-custom-value
|
||||||
.getItems=${this._getIconPickerItems}
|
.dataProvider=${ICONS_LOADED ? this._iconProvider : undefined}
|
||||||
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
.errorMessage=${this.errorMessage}
|
.errorMessage=${this.errorMessage}
|
||||||
.invalid=${this.invalid}
|
.invalid=${this.invalid}
|
||||||
.rowRenderer=${rowRenderer}
|
.renderer=${rowRenderer}
|
||||||
.icon=${this._icon}
|
icon
|
||||||
.label=${this.label}
|
@opened-changed=${this._openedChanged}
|
||||||
.value=${this._value}
|
|
||||||
.searchFn=${this._filterIcons}
|
|
||||||
.notFoundLabel=${this.hass?.localize(
|
|
||||||
"ui.components.icon-picker.no_match"
|
|
||||||
)}
|
|
||||||
popover-placement="bottom-start"
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
<slot name="start"></slot>
|
${this._value || this.placeholder
|
||||||
</ha-generic-picker>
|
? html`
|
||||||
|
<ha-icon .icon=${this._value || this.placeholder} slot="icon">
|
||||||
|
</ha-icon>
|
||||||
|
`
|
||||||
|
: html`<slot slot="icon" name="fallback"></slot>`}
|
||||||
|
</ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter can take a significant chunk of frame (up to 3-5 ms)
|
// Filter can take a significant chunk of frame (up to 3-5 ms)
|
||||||
private _filterIcons = memoizeOne(
|
private _filterIcons = memoizeOne(
|
||||||
(
|
(filter: string, iconItems: IconItem[] = ICONS) => {
|
||||||
filter: string,
|
if (!filter) {
|
||||||
filteredItems: PickerComboBoxItem[],
|
|
||||||
allItems: PickerComboBoxItem[]
|
|
||||||
): PickerComboBoxItem[] => {
|
|
||||||
const normalizedFilter = filter.toLowerCase().replace(/\s+/g, "-");
|
|
||||||
const iconItems = allItems?.length ? allItems : filteredItems;
|
|
||||||
|
|
||||||
if (!normalizedFilter.length) {
|
|
||||||
return iconItems;
|
return iconItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rankedItems: RankedIcon[] = [];
|
const filteredItems: RankedIcon[] = [];
|
||||||
|
const addIcon = (icon: string, rank: number) =>
|
||||||
|
filteredItems.push({ icon, rank });
|
||||||
|
|
||||||
// Filter and rank such that exact matches rank higher, and prefer icon name matches over keywords
|
// Filter and rank such that exact matches rank higher, and prefer icon name matches over keywords
|
||||||
for (const item of iconItems) {
|
for (const item of iconItems) {
|
||||||
const iconName = (item.id.split(":")[1] || item.id).toLowerCase();
|
if (item.parts.has(filter)) {
|
||||||
const parts = iconName.split("-");
|
addIcon(item.icon, 1);
|
||||||
const keywords = item.search_labels
|
} else if (item.keywords.includes(filter)) {
|
||||||
? Object.values(item.search_labels)
|
addIcon(item.icon, 2);
|
||||||
.filter((v): v is string => v !== null)
|
} else if (item.icon.includes(filter)) {
|
||||||
.map((v) => v.toLowerCase())
|
addIcon(item.icon, 3);
|
||||||
: [];
|
} else if (item.keywords.some((word) => word.includes(filter))) {
|
||||||
const id = item.id.toLowerCase();
|
addIcon(item.icon, 4);
|
||||||
|
|
||||||
if (parts.includes(normalizedFilter)) {
|
|
||||||
rankedItems.push({ item, rank: 1 });
|
|
||||||
} else if (keywords.includes(normalizedFilter)) {
|
|
||||||
rankedItems.push({ item, rank: 2 });
|
|
||||||
} else if (id.includes(normalizedFilter)) {
|
|
||||||
rankedItems.push({ item, rank: 3 });
|
|
||||||
} else if (keywords.some((word) => word.includes(normalizedFilter))) {
|
|
||||||
rankedItems.push({ item, rank: 4 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow preview for custom icon not in list
|
// Allow preview for custom icon not in list
|
||||||
if (rankedItems.length === 0) {
|
if (filteredItems.length === 0) {
|
||||||
rankedItems.push({
|
addIcon(filter, 0);
|
||||||
item: {
|
|
||||||
id: filter,
|
|
||||||
primary: filter,
|
|
||||||
icon: filter,
|
|
||||||
search_labels: { keyword: filter },
|
|
||||||
sorting_label: filter,
|
|
||||||
},
|
|
||||||
rank: 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rankedItems
|
return filteredItems.sort((itemA, itemB) => itemA.rank - itemB.rank);
|
||||||
.sort((itemA, itemB) => itemA.rank - itemB.rank)
|
|
||||||
.map((item) => item.item);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
protected firstUpdated() {
|
private _iconProvider = (
|
||||||
if (!ICONS_LOADED) {
|
params: ComboBoxDataProviderParams,
|
||||||
loadIcons().then(() => {
|
callback: ComboBoxDataProviderCallback<IconItem | RankedIcon>
|
||||||
this._getIconPickerItems = (): PickerComboBoxItem[] => ICONS;
|
) => {
|
||||||
|
const filteredItems = this._filterIcons(params.filter.toLowerCase(), ICONS);
|
||||||
|
const iStart = params.page * params.pageSize;
|
||||||
|
const iEnd = iStart + params.pageSize;
|
||||||
|
callback(filteredItems.slice(iStart, iEnd), filteredItems.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
private async _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
const opened = ev.detail.value;
|
||||||
|
if (opened && !ICONS_LOADED) {
|
||||||
|
await loadIcons();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,18 +194,20 @@ export class HaIconPicker extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _icon() {
|
|
||||||
return this.value?.length ? this.value : this.placeholder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _value() {
|
private get _value() {
|
||||||
return this.value || "";
|
return this.value || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
ha-generic-picker {
|
*[slot="icon"] {
|
||||||
width: 100%;
|
color: var(--primary-text-color);
|
||||||
display: block;
|
position: relative;
|
||||||
|
bottom: 2px;
|
||||||
|
}
|
||||||
|
*[slot="prefix"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
margin-inline-start: initial;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import {
|
|||||||
} from "lit/decorators";
|
} from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { getLabels, labelComboBoxKeys } from "../data/label/label_picker";
|
import type { LabelRegistryEntry } from "../data/label_registry";
|
||||||
import {
|
import {
|
||||||
createLabelRegistryEntry,
|
createLabelRegistryEntry,
|
||||||
|
getLabels,
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
type LabelRegistryEntry,
|
} from "../data/label_registry";
|
||||||
} from "../data/label/label_registry";
|
|
||||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||||
@@ -227,7 +227,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
.emptyLabel=${this.hass.localize(
|
.emptyLabel=${this.hass.localize(
|
||||||
"ui.components.label-picker.no_labels"
|
"ui.components.label-picker.no_labels"
|
||||||
@@ -238,7 +237,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
.searchKeys=${labelComboBoxKeys}
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
<slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot>
|
<slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot>
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import memoizeOne from "memoize-one";
|
|||||||
import { computeCssColor } from "../common/color/compute-color";
|
import { computeCssColor } from "../common/color/compute-color";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { stringCompare } from "../common/string/compare";
|
import { stringCompare } from "../common/string/compare";
|
||||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
import type { LabelRegistryEntry } from "../data/label_registry";
|
||||||
import {
|
import {
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
updateLabelRegistryEntry,
|
updateLabelRegistryEntry,
|
||||||
} from "../data/label/label_registry";
|
} from "../data/label_registry";
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
@@ -231,6 +231,7 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
ha-chip-set {
|
ha-chip-set {
|
||||||
|
margin-bottom: 8px;
|
||||||
background-color: var(--mdc-text-field-fill-color);
|
background-color: var(--mdc-text-field-fill-color);
|
||||||
border-bottom: 1px solid var(--ha-color-border-neutral-normal);
|
border-bottom: 1px solid var(--ha-color-border-neutral-normal);
|
||||||
border-top-right-radius: var(--ha-border-radius-sm);
|
border-top-right-radius: var(--ha-border-radius-sm);
|
||||||
|
|||||||
@@ -40,12 +40,14 @@ export const getLanguageOptions = (
|
|||||||
return {
|
return {
|
||||||
id: lang,
|
id: lang,
|
||||||
primary,
|
primary,
|
||||||
|
search_labels: [primary],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else if (locale) {
|
} else if (locale) {
|
||||||
options = languages.map((lang) => ({
|
options = languages.map((lang) => ({
|
||||||
id: lang,
|
id: lang,
|
||||||
primary: formatLanguageCode(lang, locale),
|
primary: formatLanguageCode(lang, locale),
|
||||||
|
search_labels: [formatLanguageCode(lang, locale)],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,11 +118,6 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
> `;
|
> `;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const label =
|
|
||||||
this.label ??
|
|
||||||
(this.hass?.localize("ui.components.language-picker.language") ||
|
|
||||||
"Language");
|
|
||||||
|
|
||||||
const value =
|
const value =
|
||||||
this.value ??
|
this.value ??
|
||||||
(this.required && !this.disabled ? this._getItems()[0].id : this.value);
|
(this.required && !this.disabled ? this._getItems()[0].id : this.value);
|
||||||
@@ -134,7 +131,9 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
.emptyLabel=${this.hass?.localize(
|
.emptyLabel=${this.hass?.localize(
|
||||||
"ui.components.language-picker.no_languages"
|
"ui.components.language-picker.no_languages"
|
||||||
) || "No languages available"}
|
) || "No languages available"}
|
||||||
.label=${label}
|
.placeholder=${this.label ??
|
||||||
|
(this.hass?.localize("ui.components.language-picker.language") ||
|
||||||
|
"Language")}
|
||||||
.value=${value}
|
.value=${value}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ export class HaMarkdown extends LitElement {
|
|||||||
background-color: var(--markdown-image-background-color);
|
background-color: var(--markdown-image-background-color);
|
||||||
border-radius: var(--markdown-image-border-radius);
|
border-radius: var(--markdown-image-border-radius);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
transition: height 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
p:first-child > img:first-child {
|
p:first-child > img:first-child {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
@@ -81,7 +84,8 @@ export class HaMarkdown extends LitElement {
|
|||||||
p:first-child > img:last-child {
|
p:first-child > img:last-child {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
ha-markdown-element > :is(ol, ul) {
|
:host > ul,
|
||||||
|
:host > ol {
|
||||||
padding-inline-start: var(--markdown-list-indent, revert);
|
padding-inline-start: var(--markdown-list-indent, revert);
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
@@ -132,18 +136,6 @@ export class HaMarkdown extends LitElement {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
margin: var(--ha-space-4) 0;
|
margin: var(--ha-space-4) 0;
|
||||||
}
|
}
|
||||||
table[role="presentation"] {
|
|
||||||
--markdown-table-border-collapse: separate;
|
|
||||||
--markdown-table-border-width: attr(border, 0);
|
|
||||||
--markdown-table-padding-inline: 0;
|
|
||||||
--markdown-table-padding-block: 0;
|
|
||||||
th {
|
|
||||||
vertical-align: attr(align, center);
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
vertical-align: attr(align, left);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table {
|
table {
|
||||||
border-collapse: var(--markdown-table-border-collapse, collapse);
|
border-collapse: var(--markdown-table-border-collapse, collapse);
|
||||||
}
|
}
|
||||||
@@ -151,15 +143,14 @@ export class HaMarkdown extends LitElement {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
text-align: var(--markdown-table-text-align, start);
|
text-align: start;
|
||||||
}
|
}
|
||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
border-width: var(--markdown-table-border-width, 1px);
|
border-width: var(--markdown-table-border-width, 1px);
|
||||||
border-style: var(--markdown-table-border-style, solid);
|
border-style: var(--markdown-table-border-style, solid);
|
||||||
border-color: var(--markdown-table-border-color, var(--divider-color));
|
border-color: var(--markdown-table-border-color, var(--divider-color));
|
||||||
padding-inline: var(--markdown-table-padding-inline, 0.5em);
|
padding: 0.25em 0.5em;
|
||||||
padding-block: var(--markdown-table-padding-block, 0.25em);
|
|
||||||
}
|
}
|
||||||
blockquote {
|
blockquote {
|
||||||
border-left: 4px solid var(--divider-color);
|
border-left: 4px solid var(--divider-color);
|
||||||
|
|||||||
@@ -175,10 +175,10 @@ export class HaMdDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin-top: var(--safe-area-inset-top, 0);
|
margin-top: var(--safe-area-inset-top, var(--ha-space-0));
|
||||||
margin-bottom: var(--safe-area-inset-bottom, 0);
|
margin-bottom: var(--safe-area-inset-bottom, var(--ha-space-0));
|
||||||
margin-left: var(--safe-area-inset-left, 0);
|
margin-left: var(--safe-area-inset-left, var(--ha-space-0));
|
||||||
margin-right: var(--safe-area-inset-right, 0);
|
margin-right: var(--safe-area-inset-right, var(--ha-space-0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,55 @@
|
|||||||
import { html, LitElement, nothing } from "lit";
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import type { PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { titleCase } from "../common/string/title-case";
|
import { titleCase } from "../common/string/title-case";
|
||||||
import { fetchConfig } from "../data/lovelace/config/types";
|
import { fetchConfig } from "../data/lovelace/config/types";
|
||||||
|
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
||||||
import { getPanelIcon, getPanelTitle } from "../data/panel";
|
import { getPanelIcon, getPanelTitle } from "../data/panel";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
||||||
import "./ha-generic-picker";
|
import "./ha-combo-box";
|
||||||
|
import type { HaComboBox } from "./ha-combo-box";
|
||||||
|
import "./ha-combo-box-item";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
|
||||||
|
interface NavigationItem {
|
||||||
|
path: string;
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_ITEMS: NavigationItem[] = [];
|
||||||
|
|
||||||
|
const rowRenderer: ComboBoxLitRenderer<NavigationItem> = (item) => html`
|
||||||
|
<ha-combo-box-item type="button">
|
||||||
|
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
|
||||||
|
<span slot="headline">${item.title || item.path}</span>
|
||||||
|
${item.title
|
||||||
|
? html`<span slot="supporting-text">${item.path}</span>`
|
||||||
|
: nothing}
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const createViewNavigationItem = (
|
||||||
|
prefix: string,
|
||||||
|
view: LovelaceViewRawConfig,
|
||||||
|
index: number
|
||||||
|
) => ({
|
||||||
|
path: `/${prefix}/${view.path ?? index}`,
|
||||||
|
icon: view.icon ?? "mdi:view-compact",
|
||||||
|
title: view.title ?? (view.path ? titleCase(view.path) : `${index}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
||||||
|
path: `/${panel.url_path}`,
|
||||||
|
icon: getPanelIcon(panel) || "mdi:view-dashboard",
|
||||||
|
title: getPanelTitle(hass, panel) || "",
|
||||||
|
});
|
||||||
|
|
||||||
@customElement("ha-navigation-picker")
|
@customElement("ha-navigation-picker")
|
||||||
export class HaNavigationPicker extends LitElement {
|
export class HaNavigationPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@@ -23,51 +61,46 @@ export class HaNavigationPicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@state() private _loading = true;
|
@state() private _opened = false;
|
||||||
|
|
||||||
protected firstUpdated() {
|
private navigationItemsLoaded = false;
|
||||||
|
|
||||||
|
private navigationItems: NavigationItem[] = DEFAULT_ITEMS;
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) private comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-combo-box
|
||||||
|
.hass=${this.hass}
|
||||||
|
item-value-path="path"
|
||||||
|
item-label-path="path"
|
||||||
|
.value=${this._value}
|
||||||
|
allow-custom-value
|
||||||
|
.filteredItems=${this.navigationItems}
|
||||||
|
.label=${this.label}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
.renderer=${rowRenderer}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
>
|
||||||
|
</ha-combo-box>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
if (this._opened && !this.navigationItemsLoaded) {
|
||||||
this._loadNavigationItems();
|
this._loadNavigationItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _navigationItems: PickerComboBoxItem[] = [];
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
return html`
|
|
||||||
<ha-generic-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this._loading ? undefined : this.value}
|
|
||||||
allow-custom-value
|
|
||||||
.placeholder=${this.label}
|
|
||||||
.helper=${this.helper}
|
|
||||||
.disabled=${this._loading || this.disabled}
|
|
||||||
.required=${this.required}
|
|
||||||
.getItems=${this._getItems}
|
|
||||||
.valueRenderer=${this._valueRenderer}
|
|
||||||
.customValueLabel=${this.hass.localize(
|
|
||||||
"ui.components.navigation-picker.add_custom_path"
|
|
||||||
)}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
>
|
|
||||||
</ha-generic-picker>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueRenderer = (itemId: string) => {
|
|
||||||
const item = this._navigationItems.find((navItem) => navItem.id === itemId);
|
|
||||||
return html`
|
|
||||||
${item?.icon
|
|
||||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
|
||||||
: nothing}
|
|
||||||
<span slot="headline">${item?.primary || itemId}</span>
|
|
||||||
${item?.primary
|
|
||||||
? html`<span slot="supporting-text">${itemId}</span>`
|
|
||||||
: nothing}
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _getItems = () => this._navigationItems;
|
|
||||||
|
|
||||||
private async _loadNavigationItems() {
|
private async _loadNavigationItems() {
|
||||||
|
this.navigationItemsLoaded = true;
|
||||||
|
|
||||||
const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({
|
const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({
|
||||||
id,
|
id,
|
||||||
...panel,
|
...panel,
|
||||||
@@ -91,47 +124,27 @@ export class HaNavigationPicker extends LitElement {
|
|||||||
|
|
||||||
const panelViewConfig = new Map(viewConfigs);
|
const panelViewConfig = new Map(viewConfigs);
|
||||||
|
|
||||||
this._navigationItems = [];
|
this.navigationItems = [];
|
||||||
|
|
||||||
for (const panel of panels) {
|
for (const panel of panels) {
|
||||||
const path = `/${panel.url_path}`;
|
this.navigationItems.push(createPanelNavigationItem(this.hass!, panel));
|
||||||
const panelTitle = getPanelTitle(this.hass, panel);
|
|
||||||
const primary = panelTitle || path;
|
|
||||||
this._navigationItems.push({
|
|
||||||
id: path,
|
|
||||||
primary,
|
|
||||||
secondary: panelTitle ? path : undefined,
|
|
||||||
icon: getPanelIcon(panel) || "mdi:view-dashboard",
|
|
||||||
sorting_label: [
|
|
||||||
primary.startsWith("/") ? `zzz${primary}` : primary,
|
|
||||||
path,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("_"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = panelViewConfig.get(panel.id);
|
const config = panelViewConfig.get(panel.id);
|
||||||
|
|
||||||
if (!config || !("views" in config)) continue;
|
if (!config || !("views" in config)) continue;
|
||||||
|
|
||||||
config.views.forEach((view, index) => {
|
config.views.forEach((view, index) =>
|
||||||
const viewPath = `/${panel.url_path}/${view.path ?? index}`;
|
this.navigationItems.push(
|
||||||
const viewPrimary =
|
createViewNavigationItem(panel.url_path, view, index)
|
||||||
view.title ?? (view.path ? titleCase(view.path) : `${index}`);
|
)
|
||||||
this._navigationItems.push({
|
);
|
||||||
id: viewPath,
|
|
||||||
secondary: viewPath,
|
|
||||||
icon: view.icon ?? "mdi:view-compact",
|
|
||||||
primary: viewPrimary,
|
|
||||||
sorting_label: [
|
|
||||||
viewPrimary.startsWith("/") ? `zzz${viewPrimary}` : viewPrimary,
|
|
||||||
viewPath,
|
|
||||||
].join("_"),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._loading = false;
|
this.comboBox.filteredItems = this.navigationItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues) {
|
||||||
|
return !this._opened || changedProps.has("_opened");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||||
@@ -139,18 +152,61 @@ export class HaNavigationPicker extends LitElement {
|
|||||||
this._setValue(ev.detail.value);
|
this._setValue(ev.detail.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value = "") {
|
private _setValue(value: string) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
fireEvent(
|
fireEvent(
|
||||||
this,
|
this,
|
||||||
"value-changed",
|
"value-changed",
|
||||||
{ value: this.value },
|
{ value: this._value },
|
||||||
{
|
{
|
||||||
bubbles: false,
|
bubbles: false,
|
||||||
composed: false,
|
composed: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
|
const filterString = ev.detail.value.toLowerCase();
|
||||||
|
const characterCount = filterString.length;
|
||||||
|
if (characterCount >= 2) {
|
||||||
|
const filteredItems: NavigationItem[] = [];
|
||||||
|
|
||||||
|
this.navigationItems.forEach((item) => {
|
||||||
|
if (
|
||||||
|
item.path.toLowerCase().includes(filterString) ||
|
||||||
|
item.title.toLowerCase().includes(filterString)
|
||||||
|
) {
|
||||||
|
filteredItems.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filteredItems.length > 0) {
|
||||||
|
this.comboBox.filteredItems = filteredItems;
|
||||||
|
} else {
|
||||||
|
this.comboBox.filteredItems = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.comboBox.filteredItems = this.navigationItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
ha-icon,
|
||||||
|
ha-svg-icon {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
position: relative;
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
*[slot="prefix"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
margin-inline-start: initial;
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||||
import { mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
import { mdiMagnify, mdiMinusBoxOutline } from "@mdi/js";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import {
|
import {
|
||||||
@@ -15,10 +15,7 @@ import { tinykeys } from "tinykeys";
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||||
import {
|
import { HaFuse } from "../resources/fuse";
|
||||||
multiTermSortedSearch,
|
|
||||||
type FuseWeightedKey,
|
|
||||||
} from "../resources/fuseMultiTerm";
|
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import { loadVirtualizer } from "../resources/virtualizer";
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@@ -29,32 +26,16 @@ import "./ha-icon";
|
|||||||
import "./ha-textfield";
|
import "./ha-textfield";
|
||||||
import type { HaTextField } from "./ha-textfield";
|
import type { HaTextField } from "./ha-textfield";
|
||||||
|
|
||||||
export const DEFAULT_SEARCH_KEYS: FuseWeightedKey[] = [
|
|
||||||
{
|
|
||||||
name: "primary",
|
|
||||||
weight: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "secondary",
|
|
||||||
weight: 7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "id",
|
|
||||||
weight: 3,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface PickerComboBoxItem {
|
export interface PickerComboBoxItem {
|
||||||
id: string;
|
id: string;
|
||||||
primary: string;
|
primary: string;
|
||||||
secondary?: string;
|
secondary?: string;
|
||||||
search_labels?: Record<string, string | null>;
|
search_labels?: string[];
|
||||||
sorting_label?: string;
|
sorting_label?: string;
|
||||||
icon_path?: string;
|
icon_path?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
||||||
export const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
|
||||||
|
|
||||||
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
|
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
|
||||||
item
|
item
|
||||||
@@ -92,20 +73,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
@property({ type: Boolean, attribute: "allow-custom-value" })
|
@property({ type: Boolean, attribute: "allow-custom-value" })
|
||||||
public allowCustomValue;
|
public allowCustomValue;
|
||||||
|
|
||||||
@property({ attribute: "custom-value-label" })
|
|
||||||
public customValueLabel?: string;
|
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public searchKeys?: FuseWeightedKey[];
|
|
||||||
|
|
||||||
@state() private _listScrolled = false;
|
@state() private _listScrolled = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public getItems!: (
|
public getItems?: (
|
||||||
searchString?: string,
|
searchString?: string,
|
||||||
section?: string
|
section?: string
|
||||||
) => (PickerComboBoxItem | string)[];
|
) => (PickerComboBoxItem | string)[];
|
||||||
@@ -158,8 +133,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _sectionTitle?: string;
|
@state() private _sectionTitle?: string;
|
||||||
|
|
||||||
@state() private _valuePinned = true;
|
|
||||||
|
|
||||||
private _allItems: (PickerComboBoxItem | string)[] = [];
|
private _allItems: (PickerComboBoxItem | string)[] = [];
|
||||||
|
|
||||||
private _selectedItemIndex = -1;
|
private _selectedItemIndex = -1;
|
||||||
@@ -191,15 +164,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const searchLabel =
|
|
||||||
this.label ??
|
|
||||||
(this.allowCustomValue
|
|
||||||
? (this.hass?.localize("ui.components.combo-box.search_or_custom") ??
|
|
||||||
"Search | Add custom value")
|
|
||||||
: (this.hass?.localize("ui.common.search") ?? "Search"));
|
|
||||||
|
|
||||||
return html`<ha-textfield
|
return html`<ha-textfield
|
||||||
.label=${searchLabel}
|
.label=${this.label ??
|
||||||
|
this.hass?.localize("ui.common.search") ??
|
||||||
|
"Search"}
|
||||||
@input=${this._filterChanged}
|
@input=${this._filterChanged}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
${this._renderSectionButtons()}
|
${this._renderSectionButtons()}
|
||||||
@@ -226,15 +194,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
.renderItem=${this._renderItem}
|
.renderItem=${this._renderItem}
|
||||||
style="min-height: 36px;"
|
style="min-height: 36px;"
|
||||||
class=${this._listScrolled ? "scrolled" : ""}
|
class=${this._listScrolled ? "scrolled" : ""}
|
||||||
.layout=${this.value && this._valuePinned
|
|
||||||
? {
|
|
||||||
pin: {
|
|
||||||
index: this._getInitialSelectedIndex(),
|
|
||||||
block: "center",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: undefined}
|
|
||||||
@unpinned=${this._handleUnpinned}
|
|
||||||
@scroll=${this._onScrollList}
|
@scroll=${this._onScrollList}
|
||||||
@focus=${this._focusList}
|
@focus=${this._focusList}
|
||||||
@visibilityChanged=${this._visibilityChanged}
|
@visibilityChanged=${this._visibilityChanged}
|
||||||
@@ -285,42 +244,24 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@eventOptions({ passive: true })
|
|
||||||
private _handleUnpinned() {
|
|
||||||
this._valuePinned = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getAdditionalItems = (searchString?: string) =>
|
private _getAdditionalItems = (searchString?: string) =>
|
||||||
this.getAdditionalItems?.(searchString) || [];
|
this.getAdditionalItems?.(searchString) || [];
|
||||||
|
|
||||||
private _getItems = () => {
|
private _getItems = () => {
|
||||||
let items = [...this.getItems(this._search, this.selectedSection)];
|
let items = [
|
||||||
|
...(this.getItems
|
||||||
|
? this.getItems(this._search, this.selectedSection)
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
if (!this.sections?.length) {
|
if (!this.sections?.length) {
|
||||||
items = items.sort((entityA, entityB) => {
|
items = items.sort((entityA, entityB) =>
|
||||||
const sortLabelA =
|
caseInsensitiveStringCompare(
|
||||||
typeof entityA === "string" ? entityA : entityA.sorting_label;
|
(entityA as PickerComboBoxItem).sorting_label!,
|
||||||
const sortLabelB =
|
(entityB as PickerComboBoxItem).sorting_label!,
|
||||||
typeof entityB === "string" ? entityB : entityB.sorting_label;
|
|
||||||
|
|
||||||
if (!sortLabelA || !sortLabelB) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sortLabelB) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sortLabelA) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return caseInsensitiveStringCompare(
|
|
||||||
sortLabelA,
|
|
||||||
sortLabelB,
|
|
||||||
this.hass?.locale.language ?? navigator.language
|
this.hass?.locale.language ?? navigator.language
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
@@ -338,9 +279,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
||||||
if (!item) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
if (item === "padding") {
|
if (item === "padding") {
|
||||||
return html`<div class="bottom-padding"></div>`;
|
return html`<div class="bottom-padding"></div>`;
|
||||||
}
|
}
|
||||||
@@ -401,9 +339,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
fireEvent(this, "value-changed", { value: newValue });
|
fireEvent(this, "value-changed", { value: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
private _fuseIndex = memoizeOne(
|
private _fuseIndex = memoizeOne((states: PickerComboBoxItem[]) =>
|
||||||
(states: PickerComboBoxItem[], searchKeys?: FuseWeightedKey[]) =>
|
Fuse.createIndex(["search_labels"], states)
|
||||||
Fuse.createIndex(searchKeys || DEFAULT_SEARCH_KEYS, states)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
private _filterChanged = (ev: Event) => {
|
private _filterChanged = (ev: Event) => {
|
||||||
@@ -419,25 +356,33 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = this._fuseIndex(
|
const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
|
||||||
|
const fuse = new HaFuse(
|
||||||
this._allItems as PickerComboBoxItem[],
|
this._allItems as PickerComboBoxItem[],
|
||||||
this.searchKeys
|
{
|
||||||
|
shouldSort: false,
|
||||||
|
minMatchCharLength: Math.min(searchString.length, 2),
|
||||||
|
},
|
||||||
|
index
|
||||||
);
|
);
|
||||||
|
|
||||||
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
const results = fuse.multiTermsSearch(searchString);
|
||||||
this._allItems as PickerComboBoxItem[],
|
let filteredItems = [...this._allItems];
|
||||||
searchString,
|
|
||||||
this.searchKeys || DEFAULT_SEARCH_KEYS,
|
|
||||||
(item) => item.id,
|
|
||||||
index
|
|
||||||
) as (PickerComboBoxItem | string)[];
|
|
||||||
|
|
||||||
if (!filteredItems.length) {
|
if (results) {
|
||||||
|
const items: (PickerComboBoxItem | string)[] = results.map(
|
||||||
|
(result) => result.item
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalItems = this._getAdditionalItems();
|
const additionalItems = this._getAdditionalItems();
|
||||||
filteredItems.push(...additionalItems);
|
items.push(...additionalItems);
|
||||||
|
|
||||||
|
filteredItems = items;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.searchFn) {
|
if (this.searchFn) {
|
||||||
filteredItems = this.searchFn(
|
filteredItems = this.searchFn(
|
||||||
@@ -447,23 +392,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.allowCustomValue && searchString) {
|
|
||||||
filteredItems.push({
|
|
||||||
id: searchString,
|
|
||||||
primary:
|
|
||||||
this.customValueLabel ??
|
|
||||||
this.hass?.localize("ui.components.combo-box.add_custom_item") ??
|
|
||||||
"Add custom item",
|
|
||||||
secondary: `"${searchString}"`,
|
|
||||||
icon_path: mdiPlus,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this._items = filteredItems as PickerComboBoxItem[];
|
this._items = filteredItems as PickerComboBoxItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
this._selectedItemIndex = -1;
|
this._selectedItemIndex = -1;
|
||||||
this._valuePinned = true;
|
if (this._virtualizerElement) {
|
||||||
|
this._virtualizerElement.scrollTo(0, 0);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private _toggleSection(ev: Event) {
|
private _toggleSection(ev: Event) {
|
||||||
@@ -655,25 +590,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
||||||
typeof item === "string" ? item : item?.id;
|
typeof item === "string" ? item : item.id;
|
||||||
|
|
||||||
private _getInitialSelectedIndex() {
|
|
||||||
if (!this._virtualizerElement || this._search || !this.value) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = this._virtualizerElement.items.findIndex(
|
|
||||||
(item) =>
|
|
||||||
typeof item !== "string" &&
|
|
||||||
(item as PickerComboBoxItem).id === this.value
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { consume } from "@lit/context";
|
|
||||||
import { mdiClose, mdiMenuDown } from "@mdi/js";
|
import { mdiClose, mdiMenuDown } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@@ -8,15 +7,10 @@ import {
|
|||||||
type CSSResultGroup,
|
type CSSResultGroup,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { localizeContext } from "../data/context";
|
|
||||||
import { PickerMixin } from "../mixins/picker-mixin";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
import type { HaComboBoxItem } from "./ha-combo-box-item";
|
import type { HaComboBoxItem } from "./ha-combo-box-item";
|
||||||
import "./ha-icon";
|
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -28,70 +22,45 @@ declare global {
|
|||||||
export type PickerValueRenderer = (value: string) => TemplateResult<1>;
|
export type PickerValueRenderer = (value: string) => TemplateResult<1>;
|
||||||
|
|
||||||
@customElement("ha-picker-field")
|
@customElement("ha-picker-field")
|
||||||
export class HaPickerField extends PickerMixin(LitElement) {
|
export class HaPickerField extends LitElement {
|
||||||
@property({ type: Boolean, reflect: true }) public invalid = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||||
|
public hideClearIcon = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public valueRenderer?: PickerValueRenderer;
|
||||||
|
|
||||||
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
||||||
|
|
||||||
@state()
|
|
||||||
@consume({ context: localizeContext, subscribe: true })
|
|
||||||
private localize!: HomeAssistant["localize"];
|
|
||||||
|
|
||||||
public async focus() {
|
public async focus() {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
await this.item?.focus();
|
await this.item?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const hasValue = !!this.value;
|
|
||||||
|
|
||||||
const showClearIcon =
|
const showClearIcon =
|
||||||
!!this.value && !this.required && !this.disabled && !this.hideClearIcon;
|
!!this.value && !this.required && !this.disabled && !this.hideClearIcon;
|
||||||
|
|
||||||
const placeholderText = this.placeholder ?? this.label;
|
|
||||||
|
|
||||||
const overlineLabel =
|
|
||||||
this.label && hasValue
|
|
||||||
? html`<span slot="overline"
|
|
||||||
>${this.label}${this.required ? " *" : ""}</span
|
|
||||||
>`
|
|
||||||
: nothing;
|
|
||||||
|
|
||||||
const headlineContent = hasValue
|
|
||||||
? this.valueRenderer
|
|
||||||
? this.valueRenderer(this.value ?? "")
|
|
||||||
: html`<span slot="headline">${this.value}</span>`
|
|
||||||
: placeholderText
|
|
||||||
? html`<span slot="headline" class="placeholder">
|
|
||||||
${placeholderText}${this.required ? " *" : ""}
|
|
||||||
</span>`
|
|
||||||
: nothing;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box-item
|
<ha-combo-box-item .disabled=${this.disabled} type="button" compact>
|
||||||
aria-label=${ifDefined(this.label || this.placeholder)}
|
${this.value
|
||||||
.disabled=${this.disabled}
|
? this.valueRenderer
|
||||||
type="button"
|
? this.valueRenderer(this.value)
|
||||||
compact
|
: html`<slot name="headline">${this.value}</slot>`
|
||||||
>
|
: html`
|
||||||
${this.image
|
<span slot="headline" class="placeholder">
|
||||||
? html`<img
|
${this.placeholder}
|
||||||
alt=${this.label ?? ""}
|
</span>
|
||||||
slot="start"
|
`}
|
||||||
.src=${this.image}
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
/>`
|
|
||||||
: this.icon
|
|
||||||
? html`<ha-icon slot="start" .icon=${this.icon}></ha-icon>`
|
|
||||||
: html`<slot name="start"></slot>`}
|
|
||||||
${overlineLabel}${headlineContent}
|
|
||||||
${this.unknown
|
|
||||||
? html`<div slot="supporting-text" class="unknown">
|
|
||||||
${this.unknownItemText ||
|
|
||||||
this.localize("ui.components.combo-box.unknown_item")}
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
${showClearIcon
|
${showClearIcon
|
||||||
? html`
|
? html`
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
@@ -111,7 +80,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clear(e: CustomEvent) {
|
private _clear(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
fireEvent(this, "clear");
|
fireEvent(this, "clear");
|
||||||
}
|
}
|
||||||
@@ -173,15 +142,6 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
|||||||
background-color: var(--mdc-theme-primary);
|
background-color: var(--mdc-theme-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([unknown]) ha-combo-box-item {
|
|
||||||
background-color: var(--ha-color-fill-warning-quiet-resting);
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([invalid]) ha-combo-box-item:after {
|
|
||||||
height: 2px;
|
|
||||||
background-color: var(--mdc-theme-error, var(--error-color, #b00020));
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear {
|
.clear {
|
||||||
margin: 0 -8px;
|
margin: 0 -8px;
|
||||||
--mdc-icon-button-size: 32px;
|
--mdc-icon-button-size: 32px;
|
||||||
@@ -194,14 +154,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
|||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
padding: 0 8px;
|
||||||
|
|
||||||
:host([invalid]) .placeholder {
|
|
||||||
color: var(--mdc-theme-error, var(--error-color, #b00020));
|
|
||||||
}
|
|
||||||
|
|
||||||
.unknown {
|
|
||||||
color: var(--ha-color-on-warning-normal);
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { fullEntitiesContext } from "../../data/context";
|
|||||||
import {
|
import {
|
||||||
subscribeEntityRegistry,
|
subscribeEntityRegistry,
|
||||||
type EntityRegistryEntry,
|
type EntityRegistryEntry,
|
||||||
} from "../../data/entity/entity_registry";
|
} from "../../data/entity_registry";
|
||||||
import type { Action } from "../../data/script";
|
import type { Action } from "../../data/script";
|
||||||
import { migrateAutomationAction } from "../../data/script";
|
import { migrateAutomationAction } from "../../data/script";
|
||||||
import type { ActionSelector } from "../../data/selector";
|
import type { ActionSelector } from "../../data/selector";
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export class HaAddonSelector extends LitElement {
|
|||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
|
allow-custom-entity
|
||||||
></ha-addon-picker>`;
|
></ha-addon-picker>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { html, LitElement, nothing } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
|
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||||
|
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import type { EntitySources } from "../../data/entity_sources";
|
||||||
|
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
|
||||||
|
import type { AreaSelector } from "../../data/selector";
|
||||||
import type { ConfigEntry } from "../../data/config_entries";
|
import type { ConfigEntry } from "../../data/config_entries";
|
||||||
import { getConfigEntries } from "../../data/config_entries";
|
import { getConfigEntries } from "../../data/config_entries";
|
||||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
|
||||||
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
|
|
||||||
import type { EntitySources } from "../../data/entity/entity_sources";
|
|
||||||
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
|
|
||||||
import type { AreaSelector } from "../../data/selector";
|
|
||||||
import {
|
import {
|
||||||
filterSelectorDevices,
|
filterSelectorDevices,
|
||||||
filterSelectorEntities,
|
filterSelectorEntities,
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
import type { 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 { isTemplate } from "../../common/string/has-template";
|
|
||||||
import type { ChooseSelector, Selector } from "../../data/selector";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import "../ha-button-toggle-group";
|
|
||||||
import "./ha-selector";
|
|
||||||
|
|
||||||
@customElement("ha-selector-choose")
|
|
||||||
export class HaChooseSelector extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public selector!: ChooseSelector;
|
|
||||||
|
|
||||||
@property() public value?: any;
|
|
||||||
|
|
||||||
@property() public label?: string;
|
|
||||||
|
|
||||||
@property() public helper?: string;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public localizeValue?: (key: string) => string;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = true;
|
|
||||||
|
|
||||||
@state() public _activeChoice?: string;
|
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
|
||||||
if (
|
|
||||||
changedProperties.has("selector") &&
|
|
||||||
(!this._activeChoice ||
|
|
||||||
!(this._activeChoice in this.selector.choose.choices))
|
|
||||||
) {
|
|
||||||
this._setActiveChoice();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!this._activeChoice) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selector = this._selector(this._activeChoice);
|
|
||||||
const value = this._value(this._activeChoice);
|
|
||||||
|
|
||||||
return html`<div class="multi-header">
|
|
||||||
<span>${this.label}${this.required ? "*" : ""}</span>
|
|
||||||
<ha-button-toggle-group
|
|
||||||
size="small"
|
|
||||||
.buttons=${this._toggleButtons(
|
|
||||||
this.selector.choose.choices,
|
|
||||||
this.selector.choose.translation_key
|
|
||||||
)}
|
|
||||||
.active=${this._activeChoice}
|
|
||||||
@value-changed=${this._choiceChanged}
|
|
||||||
></ha-button-toggle-group>
|
|
||||||
</div>
|
|
||||||
<ha-selector
|
|
||||||
.hass=${this.hass}
|
|
||||||
.selector=${selector}
|
|
||||||
.value=${value}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
.required=${this.required}
|
|
||||||
@value-changed=${this._handleValueChanged}
|
|
||||||
.helper=${this.helper}
|
|
||||||
></ha-selector>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleButtons = memoizeOne(
|
|
||||||
(choices: ChooseSelector["choose"]["choices"], translationKey?: string) =>
|
|
||||||
Object.keys(choices).map((choice) => ({
|
|
||||||
label:
|
|
||||||
this.localizeValue && translationKey
|
|
||||||
? this.localizeValue(`${translationKey}.choices.${choice}`)
|
|
||||||
: choice,
|
|
||||||
value: choice,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
private _choiceChanged(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const value =
|
|
||||||
typeof this.value === "object"
|
|
||||||
? this.value
|
|
||||||
: {
|
|
||||||
[this._activeChoice!]: this.value,
|
|
||||||
};
|
|
||||||
this._activeChoice = ev.detail?.value || ev.target.value;
|
|
||||||
fireEvent(this, "value-changed", {
|
|
||||||
value: {
|
|
||||||
...value,
|
|
||||||
active_choice: this._activeChoice,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleValueChanged(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const value = typeof this.value === "object" ? this.value : {};
|
|
||||||
fireEvent(this, "value-changed", {
|
|
||||||
value: {
|
|
||||||
...value,
|
|
||||||
[this._activeChoice!]: ev.detail.value,
|
|
||||||
active_choice: this._activeChoice,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _selector(choice?: string): Selector {
|
|
||||||
const choices = this.selector.choose.choices;
|
|
||||||
|
|
||||||
choice = choice || this.value?.active_choice;
|
|
||||||
|
|
||||||
if (choice && choice in choices) {
|
|
||||||
return choices[choice].selector;
|
|
||||||
}
|
|
||||||
|
|
||||||
return choices[Object.keys(choices)[0]].selector;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _value(choice?: string): any {
|
|
||||||
if (!this.value) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return typeof this.value === "object"
|
|
||||||
? this.value[choice || this.value.active_choice]
|
|
||||||
: this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setActiveChoice() {
|
|
||||||
if (this.value) {
|
|
||||||
if (typeof this.value === "object") {
|
|
||||||
if (this.value.active_choice in this.selector.choose.choices) {
|
|
||||||
this._activeChoice = this.value.active_choice;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const typeofValue = typeof this.value;
|
|
||||||
const selectorTypes = Object.values(this.selector.choose.choices).map(
|
|
||||||
(choice) => Object.keys(choice.selector)[0]
|
|
||||||
);
|
|
||||||
if (typeofValue === "number" && selectorTypes.includes("number")) {
|
|
||||||
this._activeChoice = Object.keys(this.selector.choose.choices)[
|
|
||||||
selectorTypes.indexOf("number")
|
|
||||||
];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeofValue === "string" &&
|
|
||||||
isTemplate(this.value) &&
|
|
||||||
selectorTypes.includes("template")
|
|
||||||
) {
|
|
||||||
this._activeChoice = Object.keys(this.selector.choose.choices)[
|
|
||||||
selectorTypes.indexOf("template")
|
|
||||||
];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeofValue === "string" &&
|
|
||||||
this.value.includes(".") &&
|
|
||||||
selectorTypes.includes("entity")
|
|
||||||
) {
|
|
||||||
this._activeChoice = Object.keys(this.selector.choose.choices)[
|
|
||||||
selectorTypes.indexOf("entity")
|
|
||||||
];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeofValue === "string" && selectorTypes.includes("text")) {
|
|
||||||
this._activeChoice = Object.keys(this.selector.choose.choices)[
|
|
||||||
selectorTypes.indexOf("text")
|
|
||||||
];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._activeChoice = Object.keys(this.selector.choose.choices)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
.multi-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--ha-space-2);
|
|
||||||
}
|
|
||||||
ha-button-toggle-group {
|
|
||||||
display: block;
|
|
||||||
justify-self: end;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-selector-choose": HaChooseSelector;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,6 +29,7 @@ export class HaConfigEntrySelector extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.integration=${this.selector.config_entry?.integration}
|
.integration=${this.selector.config_entry?.integration}
|
||||||
|
allow-custom-entity
|
||||||
></ha-config-entry-picker>`;
|
></ha-config-entry-picker>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||||
|
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||||
|
import type { EntitySources } from "../../data/entity_sources";
|
||||||
|
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
|
||||||
|
import type { DeviceSelector } from "../../data/selector";
|
||||||
import type { ConfigEntry } from "../../data/config_entries";
|
import type { ConfigEntry } from "../../data/config_entries";
|
||||||
import { getConfigEntries } from "../../data/config_entries";
|
import { getConfigEntries } from "../../data/config_entries";
|
||||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
|
||||||
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
|
|
||||||
import type { EntitySources } from "../../data/entity/entity_sources";
|
|
||||||
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
|
|
||||||
import type { DeviceSelector } from "../../data/selector";
|
|
||||||
import {
|
import {
|
||||||
filterSelectorDevices,
|
filterSelectorDevices,
|
||||||
filterSelectorEntities,
|
filterSelectorEntities,
|
||||||
@@ -107,6 +107,7 @@ export class HaDeviceSelector extends LitElement {
|
|||||||
.placeholder=${this.placeholder}
|
.placeholder=${this.placeholder}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
|
allow-custom-entity
|
||||||
></ha-device-picker>
|
></ha-device-picker>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { html, LitElement, nothing } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import type { EntitySources } from "../../data/entity/entity_sources";
|
import type { EntitySources } from "../../data/entity_sources";
|
||||||
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
|
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
|
||||||
import type { EntitySelector } from "../../data/selector";
|
import type { EntitySelector } from "../../data/selector";
|
||||||
import {
|
import {
|
||||||
computeCreateDomains,
|
|
||||||
filterSelectorEntities,
|
filterSelectorEntities,
|
||||||
|
computeCreateDomains,
|
||||||
} from "../../data/selector";
|
} from "../../data/selector";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../entity/ha-entities-picker";
|
import "../entity/ha-entities-picker";
|
||||||
@@ -66,14 +66,15 @@ export class HaEntitySelector extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.placeholder=${this.placeholder}
|
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.includeEntities=${this.selector.entity?.include_entities}
|
.includeEntities=${this.selector.entity?.include_entities}
|
||||||
.excludeEntities=${this.selector.entity?.exclude_entities}
|
.excludeEntities=${this.selector.entity?.exclude_entities}
|
||||||
.entityFilter=${this._filterEntities}
|
.entityFilter=${this._filterEntities}
|
||||||
.createDomains=${this._createDomains}
|
.createDomains=${this._createDomains}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
|
allow-custom-entity
|
||||||
></ha-entity-picker>`;
|
></ha-entity-picker>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,13 +83,13 @@ export class HaEntitySelector extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.placeholder=${this.placeholder}
|
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.includeEntities=${this.selector.entity.include_entities}
|
.includeEntities=${this.selector.entity.include_entities}
|
||||||
.excludeEntities=${this.selector.entity.exclude_entities}
|
.excludeEntities=${this.selector.entity.exclude_entities}
|
||||||
.reorder=${this.selector.entity.reorder ?? false}
|
.reorder=${this.selector.entity.reorder ?? false}
|
||||||
.entityFilter=${this._filterEntities}
|
.entityFilter=${this._filterEntities}
|
||||||
.createDomains=${this._createDomains}
|
.createDomains=${this._createDomains}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
></ha-entities-picker>
|
></ha-entities-picker>
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { html, LitElement, nothing } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
|
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||||
|
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import type { EntitySources } from "../../data/entity_sources";
|
||||||
|
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
|
||||||
|
import type { FloorSelector } from "../../data/selector";
|
||||||
import type { ConfigEntry } from "../../data/config_entries";
|
import type { ConfigEntry } from "../../data/config_entries";
|
||||||
import { getConfigEntries } from "../../data/config_entries";
|
import { getConfigEntries } from "../../data/config_entries";
|
||||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
|
||||||
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
|
|
||||||
import type { EntitySources } from "../../data/entity/entity_sources";
|
|
||||||
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
|
|
||||||
import type { FloorSelector } from "../../data/selector";
|
|
||||||
import {
|
import {
|
||||||
filterSelectorDevices,
|
filterSelectorDevices,
|
||||||
filterSelectorEntities,
|
filterSelectorEntities,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export class HaIconSelector extends LitElement {
|
|||||||
${!placeholder && stateObj
|
${!placeholder && stateObj
|
||||||
? html`
|
? html`
|
||||||
<ha-state-icon
|
<ha-state-icon
|
||||||
slot="start"
|
slot="fallback"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.stateObj=${stateObj}
|
.stateObj=${stateObj}
|
||||||
></ha-state-icon>
|
></ha-state-icon>
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ export class HaLabelSelector extends LitElement {
|
|||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
|
||||||
@value-changed=${this._handleChange}
|
@value-changed=${this._handleChange}
|
||||||
>
|
>
|
||||||
</ha-labels-picker>
|
</ha-labels-picker>
|
||||||
@@ -50,7 +49,6 @@ export class HaLabelSelector extends LitElement {
|
|||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
|
||||||
@value-changed=${this._handleChange}
|
@value-changed=${this._handleChange}
|
||||||
>
|
>
|
||||||
</ha-label-picker>
|
</ha-label-picker>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { mdiDragHorizontalVariant } from "@mdi/js";
|
import { mdiDragHorizontalVariant } from "@mdi/js";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
@@ -12,8 +11,9 @@ import type { HomeAssistant } from "../../types";
|
|||||||
import "../chips/ha-chip-set";
|
import "../chips/ha-chip-set";
|
||||||
import "../chips/ha-input-chip";
|
import "../chips/ha-input-chip";
|
||||||
import "../ha-checkbox";
|
import "../ha-checkbox";
|
||||||
|
import "../ha-combo-box";
|
||||||
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
import "../ha-formfield";
|
import "../ha-formfield";
|
||||||
import "../ha-generic-picker";
|
|
||||||
import "../ha-input-helper-text";
|
import "../ha-input-helper-text";
|
||||||
import "../ha-list-item";
|
import "../ha-list-item";
|
||||||
import "../ha-radio";
|
import "../ha-radio";
|
||||||
@@ -40,6 +40,8 @@ export class HaSelectSelector extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public required = true;
|
@property({ type: Boolean }) public required = true;
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) private comboBox!: HaComboBox;
|
||||||
|
|
||||||
private _itemMoved(ev: CustomEvent): void {
|
private _itemMoved(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const { oldIndex, newIndex } = ev.detail;
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
@@ -57,8 +59,15 @@ export class HaSelectSelector extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _filter = "";
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const options = this._getOptions(this.selector);
|
const options =
|
||||||
|
this.selector.select?.options?.map((option) =>
|
||||||
|
typeof option === "object"
|
||||||
|
? (option as SelectOption)
|
||||||
|
: ({ value: option, label: option } as SelectOption)
|
||||||
|
) || [];
|
||||||
|
|
||||||
const translationKey = this.selector.select?.translation_key;
|
const translationKey = this.selector.select?.translation_key;
|
||||||
|
|
||||||
@@ -156,6 +165,10 @@ export class HaSelectSelector extends LitElement {
|
|||||||
const value =
|
const value =
|
||||||
!this.value || this.value === "" ? [] : ensureArray(this.value);
|
!this.value || this.value === "" ? [] : ensureArray(this.value);
|
||||||
|
|
||||||
|
const optionItems = options.filter(
|
||||||
|
(option) => !option.disabled && !value?.includes(option.value)
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${value?.length
|
${value?.length
|
||||||
? html`
|
? html`
|
||||||
@@ -199,33 +212,50 @@ export class HaSelectSelector extends LitElement {
|
|||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
<ha-generic-picker
|
<ha-combo-box
|
||||||
|
item-value-path="value"
|
||||||
|
item-label-path="label"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required && !value.length}
|
.required=${this.required && !value.length}
|
||||||
.value=${""}
|
.value=${""}
|
||||||
.addButtonLabel=${this.label}
|
.items=${optionItems}
|
||||||
.getItems=${this._getItems(options, value, true)}
|
|
||||||
.allowCustomValue=${this.selector.select.custom_value ?? false}
|
.allowCustomValue=${this.selector.select.custom_value ?? false}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
@value-changed=${this._comboBoxValueChanged}
|
@value-changed=${this._comboBoxValueChanged}
|
||||||
></ha-generic-picker>
|
@opened-changed=${this._openedChanged}
|
||||||
|
></ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selector.select?.custom_value) {
|
if (this.selector.select?.custom_value) {
|
||||||
|
if (
|
||||||
|
this.value !== undefined &&
|
||||||
|
!Array.isArray(this.value) &&
|
||||||
|
!options.find((option) => option.value === this.value)
|
||||||
|
) {
|
||||||
|
options.unshift({ value: this.value, label: this.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionItems = options.filter((option) => !option.disabled);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-combo-box
|
||||||
|
item-value-path="value"
|
||||||
|
item-label-path="label"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.getItems=${this._getItems(options)}
|
.items=${optionItems}
|
||||||
.value=${this.value as string | undefined}
|
.value=${this.value}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
@value-changed=${this._comboBoxValueChanged}
|
@value-changed=${this._comboBoxValueChanged}
|
||||||
allow-custom-value
|
@opened-changed=${this._openedChanged}
|
||||||
></ha-generic-picker>
|
></ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +274,7 @@ export class HaSelectSelector extends LitElement {
|
|||||||
>
|
>
|
||||||
${options.map(
|
${options.map(
|
||||||
(item: SelectOption) => html`
|
(item: SelectOption) => html`
|
||||||
<ha-list-item .value=${item.value} .disabled=${!!item.disabled}
|
<ha-list-item .value=${item.value} .disabled=${item.disabled}
|
||||||
>${item.label}</ha-list-item
|
>${item.label}</ha-list-item
|
||||||
>
|
>
|
||||||
`
|
`
|
||||||
@@ -261,30 +291,6 @@ export class HaSelectSelector extends LitElement {
|
|||||||
: "";
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getOptions = memoizeOne(
|
|
||||||
(selector: SelectSelector) =>
|
|
||||||
selector.select?.options?.map((option) =>
|
|
||||||
typeof option === "object"
|
|
||||||
? (option as SelectOption)
|
|
||||||
: ({ value: option, label: option } as SelectOption)
|
|
||||||
) || []
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getItems = memoizeOne(
|
|
||||||
(options: SelectOption[], value?: string[], multiple = false) => {
|
|
||||||
const filteredOptions = options.filter((option) =>
|
|
||||||
!option.disabled && !multiple ? true : !value?.includes(option.value)
|
|
||||||
);
|
|
||||||
|
|
||||||
return () =>
|
|
||||||
filteredOptions.map((option) => ({
|
|
||||||
id: option.value,
|
|
||||||
primary: option.label,
|
|
||||||
sorting_label: option.label,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private get _mode(): "list" | "dropdown" | "box" {
|
private get _mode(): "list" | "dropdown" | "box" {
|
||||||
return (
|
return (
|
||||||
this.selector.select?.mode ||
|
this.selector.select?.mode ||
|
||||||
@@ -349,6 +355,8 @@ export class HaSelectSelector extends LitElement {
|
|||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
|
await this.updateComplete;
|
||||||
|
this._filterChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _comboBoxValueChanged(ev: CustomEvent): void {
|
private _comboBoxValueChanged(ev: CustomEvent): void {
|
||||||
@@ -366,17 +374,49 @@ export class HaSelectSelector extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentValue = !this.value ? [] : ensureArray(this.value);
|
const currentValue =
|
||||||
|
!this.value || this.value === "" ? [] : ensureArray(this.value);
|
||||||
|
|
||||||
if (newValue !== undefined && currentValue.includes(newValue)) {
|
if (newValue !== undefined && currentValue.includes(newValue)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this._filterChanged();
|
||||||
|
this.comboBox.setInputValue("");
|
||||||
|
}, 0);
|
||||||
|
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: [...currentValue, newValue],
|
value: [...currentValue, newValue],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev?: CustomEvent): void {
|
||||||
|
if (ev?.detail.value) {
|
||||||
|
this._filterChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterChanged(ev?: CustomEvent): void {
|
||||||
|
this._filter = ev?.detail.value || "";
|
||||||
|
|
||||||
|
const filteredItems = this.comboBox.items?.filter((item) => {
|
||||||
|
const label = item.label || item.value;
|
||||||
|
return label.toLowerCase().includes(this._filter?.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._filter &&
|
||||||
|
this.selector.select?.custom_value &&
|
||||||
|
filteredItems &&
|
||||||
|
!filteredItems.some((item) => (item.label || item.value) === this._filter)
|
||||||
|
) {
|
||||||
|
filteredItems.unshift({ label: this._filter, value: this._filter });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.comboBox.filteredItems = filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import { css, html, LitElement, nothing } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||||
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
|
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||||
import type { EntitySources } from "../../data/entity/entity_sources";
|
import type { EntitySources } from "../../data/entity_sources";
|
||||||
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
|
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
|
||||||
import type { TargetSelector } from "../../data/selector";
|
import type { TargetSelector } from "../../data/selector";
|
||||||
import {
|
import {
|
||||||
computeCreateDomains,
|
|
||||||
filterSelectorDevices,
|
filterSelectorDevices,
|
||||||
filterSelectorEntities,
|
filterSelectorEntities,
|
||||||
|
computeCreateDomains,
|
||||||
} from "../../data/selector";
|
} from "../../data/selector";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-target-picker";
|
import "../ha-target-picker";
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user