custom_components/bahmcloud_store/panel/panel.js aktualisiert

This commit is contained in:
2026-01-15 09:46:41 +00:00
parent bae4d0b84f
commit 77b4522e3c

View File

@@ -5,9 +5,7 @@ class BahmcloudStorePanel extends HTMLElement {
this._hass = null; this._hass = null;
// Views: store | manage | about | detail
this._view = "store"; this._view = "store";
this._data = null; this._data = null;
this._loading = true; this._loading = true;
this._error = null; this._error = null;
@@ -15,11 +13,9 @@ class BahmcloudStorePanel extends HTMLElement {
this._customAddUrl = ""; this._customAddUrl = "";
this._customAddName = ""; this._customAddName = "";
// Store filtering
this._search = ""; this._search = "";
this._category = "all"; this._category = "all";
// Detail view state
this._detailRepoId = null; this._detailRepoId = null;
this._detailRepo = null; this._detailRepo = null;
this._readmeLoading = false; this._readmeLoading = false;
@@ -81,7 +77,6 @@ class BahmcloudStorePanel extends HTMLElement {
} }
_goBack() { _goBack() {
// If we're in detail, go back to store list first.
if (this._view === "detail") { if (this._view === "detail") {
this._view = "store"; this._view = "store";
this._detailRepoId = null; this._detailRepoId = null;
@@ -244,10 +239,7 @@ class BahmcloudStorePanel extends HTMLElement {
border-color:var(--bcs-accent); border-color:var(--bcs-accent);
background: color-mix(in srgb, var(--bcs-accent) 16%, var(--card-background-color)); background: color-mix(in srgb, var(--bcs-accent) 16%, var(--card-background-color));
} }
button:disabled{ button:disabled{ opacity: 0.55; cursor: not-allowed; }
opacity: 0.55;
cursor: not-allowed;
}
.card{ .card{
border:1px solid var(--divider-color); border:1px solid var(--divider-color);
@@ -277,14 +269,8 @@ class BahmcloudStorePanel extends HTMLElement {
.error{ color:#b00020; white-space:pre-wrap; margin-top:10px; } .error{ color:#b00020; white-space:pre-wrap; margin-top:10px; }
.grid2{ .grid2{ display:grid; grid-template-columns: 1fr; gap: 12px; }
display:grid; @media (min-width: 900px){ .grid2{ grid-template-columns: 1.2fr 0.8fr; } }
grid-template-columns: 1fr;
gap: 12px;
}
@media (min-width: 900px){
.grid2{ grid-template-columns: 1.2fr 0.8fr; }
}
.filters{ .filters{
display:flex; display:flex;
@@ -309,7 +295,6 @@ class BahmcloudStorePanel extends HTMLElement {
a{ color:var(--bcs-accent); text-decoration:none; } a{ color:var(--bcs-accent); text-decoration:none; }
a:hover{ text-decoration:underline; } a:hover{ text-decoration:underline; }
/* FABs (detail view) */
.fabs{ .fabs{
position: fixed; position: fixed;
right: 18px; right: 18px;
@@ -337,12 +322,8 @@ class BahmcloudStorePanel extends HTMLElement {
border-color: var(--bcs-accent); border-color: var(--bcs-accent);
background: color-mix(in srgb, var(--bcs-accent) 18%, var(--card-background-color)); background: color-mix(in srgb, var(--bcs-accent) 18%, var(--card-background-color));
} }
.fab[disabled]{ .fab[disabled]{ opacity: .55; cursor: not-allowed; }
opacity: .55;
cursor: not-allowed;
}
/* README fallback pre */
pre.readme{ pre.readme{
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
@@ -351,6 +332,9 @@ class BahmcloudStorePanel extends HTMLElement {
font-size: 12.5px; font-size: 12.5px;
line-height: 1.5; line-height: 1.5;
} }
details{ margin-top: 10px; }
summary{ cursor:pointer; color: var(--bcs-accent); font-weight: 800; }
</style> </style>
<div class="mobilebar"> <div class="mobilebar">
@@ -392,8 +376,7 @@ class BahmcloudStorePanel extends HTMLElement {
}); });
} }
// ✅ CRITICAL FIX: prevent HA global shortcuts (Assist/Command Palette) from // Prevent HA global shortcuts while typing inside panel inputs
// stealing keystrokes while typing in inputs inside this panel.
const stopIfFormField = (e) => { const stopIfFormField = (e) => {
const t = e.composedPath ? e.composedPath()[0] : e.target; const t = e.composedPath ? e.composedPath()[0] : e.target;
if (!t) return; if (!t) return;
@@ -410,14 +393,47 @@ class BahmcloudStorePanel extends HTMLElement {
} }
}; };
// Use capture phase so we intercept before HA handlers.
root.addEventListener("keydown", stopIfFormField, true); root.addEventListener("keydown", stopIfFormField, true);
root.addEventListener("keyup", stopIfFormField, true); root.addEventListener("keyup", stopIfFormField, true);
root.addEventListener("keypress", stopIfFormField, true); root.addEventListener("keypress", stopIfFormField, true);
} }
_captureFocusState() {
const root = this.shadowRoot;
const ae = root.activeElement;
if (!ae || !ae.id) return null;
const supported = new Set(["searchInput", "categorySelect", "addUrl", "addName"]);
if (!supported.has(ae.id)) return null;
const state = {
id: ae.id,
value: ae.value,
selectionStart: typeof ae.selectionStart === "number" ? ae.selectionStart : null,
selectionEnd: typeof ae.selectionEnd === "number" ? ae.selectionEnd : null,
};
return state;
}
_restoreFocusState(state) {
if (!state) return;
const root = this.shadowRoot;
const el = root.getElementById(state.id);
if (!el) return;
try {
el.focus({ preventScroll: true });
if (state.selectionStart !== null && state.selectionEnd !== null && typeof el.setSelectionRange === "function") {
el.setSelectionRange(state.selectionStart, state.selectionEnd);
}
} catch (_) {}
}
_update() { _update() {
const root = this.shadowRoot; const root = this.shadowRoot;
const focusState = this._captureFocusState();
const content = root.getElementById("content"); const content = root.getElementById("content");
const err = root.getElementById("error"); const err = root.getElementById("error");
const subtitle = root.getElementById("subtitle"); const subtitle = root.getElementById("subtitle");
@@ -437,34 +453,39 @@ class BahmcloudStorePanel extends HTMLElement {
if (this._loading) { if (this._loading) {
content.innerHTML = `<div class="card">Loading…</div>`; content.innerHTML = `<div class="card">Loading…</div>`;
this._restoreFocusState(focusState);
return; return;
} }
if (!this._data) { if (!this._data) {
content.innerHTML = `<div class="card">No data.</div>`; content.innerHTML = `<div class="card">No data.</div>`;
this._restoreFocusState(focusState);
return; return;
} }
if (this._view === "store") { if (this._view === "store") {
content.innerHTML = this._renderStore(); content.innerHTML = this._renderStore();
this._wireStore(); this._wireStore();
this._restoreFocusState(focusState);
return; return;
} }
if (this._view === "manage") { if (this._view === "manage") {
content.innerHTML = this._renderManage(); content.innerHTML = this._renderManage();
this._wireManage(); this._wireManage();
this._restoreFocusState(focusState);
return; return;
} }
if (this._view === "about") { if (this._view === "about") {
content.innerHTML = this._renderAbout(); content.innerHTML = this._renderAbout();
this._restoreFocusState(focusState);
return; return;
} }
// detail
content.innerHTML = this._renderDetail(); content.innerHTML = this._renderDetail();
this._wireDetail(); this._wireDetail();
this._restoreFocusState(focusState);
} }
_renderStore() { _renderStore() {
@@ -488,17 +509,20 @@ class BahmcloudStorePanel extends HTMLElement {
...categories.map((c) => `<option value="${this._esc(c)}"${this._category === c ? " selected" : ""}>${this._esc(c)}</option>`), ...categories.map((c) => `<option value="${this._esc(c)}"${this._category === c ? " selected" : ""}>${this._esc(c)}</option>`),
].join(""); ].join("");
const rows = filtered const rows = filtered.map((r) => {
.map((r) => {
const badge = r.source === "custom" const badge = r.source === "custom"
? `<span class="badge custom">Custom</span>` ? `<span class="badge custom">Custom</span>`
: `<span class="badge">Index</span>`; : `<span class="badge">Index</span>`;
const desc = r.description || r.meta_description || r.provider_description || "No description available."; const desc = r.description || "No description available.";
const creator = r.owner ? `Creator: ${r.owner}` : "Creator: -"; const creator = r.owner ? `Creator: ${r.owner}` : "Creator: -";
const cat = r.category ? `Category: ${r.category}` : null; const cat = r.category ? `Category: ${r.category}` : null;
const metaSrc = r.meta_source ? `Meta: ${r.meta_source}` : null; const metaSrc = r.meta_source ? `Meta: ${r.meta_source}` : null;
const lineBits = [creator, cat, metaSrc].filter(Boolean);
const latest = r.latest_version ? `Latest: ${r.latest_version}` : "Latest: unknown";
const lineBits = [creator, latest, cat, metaSrc].filter(Boolean);
return ` return `
<div class="card clickable" data-repo="${this._esc(r.id)}"> <div class="card clickable" data-repo="${this._esc(r.id)}">
@@ -512,8 +536,7 @@ class BahmcloudStorePanel extends HTMLElement {
</div> </div>
</div> </div>
`; `;
}) }).join("");
.join("");
return ` return `
<div class="filters"> <div class="filters">
@@ -563,10 +586,13 @@ class BahmcloudStorePanel extends HTMLElement {
? `<span class="badge custom">Custom</span>` ? `<span class="badge custom">Custom</span>`
: `<span class="badge">Index</span>`; : `<span class="badge">Index</span>`;
const desc = r.description || r.meta_description || r.provider_description || "No description available."; const desc = r.description || "No description available.";
const latest = r.latest_version ? `Latest: ${r.latest_version}` : "Latest: unknown";
const infoBits = [ const infoBits = [
r.owner ? `Creator: ${r.owner}` : "Creator: -", r.owner ? `Creator: ${r.owner}` : "Creator: -",
latest,
r.provider ? `Provider: ${r.provider}` : null, r.provider ? `Provider: ${r.provider}` : null,
r.category ? `Category: ${r.category}` : null, r.category ? `Category: ${r.category}` : null,
r.meta_author ? `Author: ${r.meta_author}` : null, r.meta_author ? `Author: ${r.meta_author}` : null,
@@ -580,8 +606,14 @@ class BahmcloudStorePanel extends HTMLElement {
? ` ? `
<div class="card"> <div class="card">
<div><strong>README</strong></div> <div><strong>README</strong></div>
<div class="muted small" style="margin-top:6px;">Rendered Markdown (fallback to raw text if needed).</div>
<div id="readmeContainer" style="margin-top:12px;"></div> <div id="readmeContainer" style="margin-top:12px;"></div>
<details>
<summary>Show raw Markdown</summary>
<div style="margin-top:10px;">
<pre class="readme">${this._esc(this._readmeText)}</pre>
</div>
</details>
</div> </div>
` `
: ` : `
@@ -628,9 +660,6 @@ class BahmcloudStorePanel extends HTMLElement {
} }
_wireDetail() { _wireDetail() {
// Render README into container:
// 1) Try ha-markdown (if available)
// 2) Fallback: raw markdown in <pre>
const root = this.shadowRoot; const root = this.shadowRoot;
const container = root.getElementById("readmeContainer"); const container = root.getElementById("readmeContainer");
if (!container) return; if (!container) return;
@@ -639,19 +668,22 @@ class BahmcloudStorePanel extends HTMLElement {
if (!this._readmeText) return; if (!this._readmeText) return;
// Try HA markdown element (best effort) // Only attempt ha-markdown if the custom element exists
const hasHaMarkdown = typeof customElements !== "undefined" && !!customElements.get("ha-markdown");
if (hasHaMarkdown) {
try { try {
const el = document.createElement("ha-markdown"); const el = document.createElement("ha-markdown");
// some HA builds need hass set before content
try { el.hass = this._hass; } catch (_) {} try { el.hass = this._hass; } catch (_) {}
el.content = this._readmeText; el.content = this._readmeText;
container.appendChild(el); container.appendChild(el);
return; return;
} catch (_) { } catch (_) {
// fall through // fall through to raw fallback (details already exists)
}
} }
// Fallback: raw text // If ha-markdown is missing, show a minimal rendered area:
const pre = document.createElement("pre"); const pre = document.createElement("pre");
pre.className = "readme"; pre.className = "readme";
pre.textContent = this._readmeText; pre.textContent = this._readmeText;
@@ -697,8 +729,7 @@ class BahmcloudStorePanel extends HTMLElement {
const repos = Array.isArray(this._data.repos) ? this._data.repos : []; const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
const custom = repos.filter((r) => r.source === "custom"); const custom = repos.filter((r) => r.source === "custom");
const list = custom const list = custom.map((r) => {
.map((r) => {
return ` return `
<div class="card"> <div class="card">
<div class="row"> <div class="row">
@@ -712,8 +743,7 @@ class BahmcloudStorePanel extends HTMLElement {
</div> </div>
</div> </div>
`; `;
}) }).join("");
.join("");
return ` return `
<div class="card"> <div class="card">