custom_components/bahmcloud_store/panel/panel.js aktualisiert
This commit is contained in:
@@ -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,32 +509,34 @@ 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 cat = r.category ? `Category: ${r.category}` : null;
|
|
||||||
const metaSrc = r.meta_source ? `Meta: ${r.meta_source}` : null;
|
|
||||||
const lineBits = [creator, cat, metaSrc].filter(Boolean);
|
|
||||||
|
|
||||||
return `
|
const creator = r.owner ? `Creator: ${r.owner}` : "Creator: -";
|
||||||
<div class="card clickable" data-repo="${this._esc(r.id)}">
|
const cat = r.category ? `Category: ${r.category}` : null;
|
||||||
<div class="row">
|
const metaSrc = r.meta_source ? `Meta: ${r.meta_source}` : null;
|
||||||
<div>
|
|
||||||
<div><strong>${this._esc(r.name)}</strong></div>
|
const latest = r.latest_version ? `Latest: ${r.latest_version}` : "Latest: unknown";
|
||||||
<div class="muted">${this._esc(desc)}</div>
|
|
||||||
<div class="muted small">${this._esc(lineBits.join(" · "))}</div>
|
const lineBits = [creator, latest, cat, metaSrc].filter(Boolean);
|
||||||
</div>
|
|
||||||
${badge}
|
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>
|
</div>
|
||||||
|
${badge}
|
||||||
</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
|
||||||
try {
|
const hasHaMarkdown = typeof customElements !== "undefined" && !!customElements.get("ha-markdown");
|
||||||
const el = document.createElement("ha-markdown");
|
|
||||||
// some HA builds need hass set before content
|
if (hasHaMarkdown) {
|
||||||
try { el.hass = this._hass; } catch (_) {}
|
try {
|
||||||
el.content = this._readmeText;
|
const el = document.createElement("ha-markdown");
|
||||||
container.appendChild(el);
|
try { el.hass = this._hass; } catch (_) {}
|
||||||
return;
|
el.content = this._readmeText;
|
||||||
} catch (_) {
|
container.appendChild(el);
|
||||||
// fall through
|
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");
|
const pre = document.createElement("pre");
|
||||||
pre.className = "readme";
|
pre.className = "readme";
|
||||||
pre.textContent = this._readmeText;
|
pre.textContent = this._readmeText;
|
||||||
@@ -697,23 +729,21 @@ 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">
|
<div>
|
||||||
<div>
|
<div><strong>${this._esc(r.name)}</strong></div>
|
||||||
<div><strong>${this._esc(r.name)}</strong></div>
|
<div class="muted">${this._esc(r.url)}</div>
|
||||||
<div class="muted">${this._esc(r.url)}</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<button class="primary" data-remove="${this._esc(r.id)}">Remove</button>
|
||||||
<button class="primary" data-remove="${this._esc(r.id)}">Remove</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
</div>
|
||||||
})
|
`;
|
||||||
.join("");
|
}).join("");
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
Reference in New Issue
Block a user