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