mirror of
https://github.com/thekiwismarthome/shopping-list-manager.git
synced 2026-06-30 21:46:30 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0664f5331a | |||
| 30d8d8defd | |||
| 07329323bf | |||
| 7cf703a9ac | |||
| 1edf4b8eea | |||
| 6376c5148d | |||
| 54cbcd5298 | |||
| 5c093d2f3b | |||
| aef90630db | |||
| a79e4e7171 |
@@ -1,36 +1,135 @@
|
|||||||
## Installation
|
# Shopping List Manager
|
||||||
|
|
||||||
### 1. Install via HACS
|
A custom Home Assistant integration that provides an enhanced shopping list experience, including a companion Lovelace card for managing items directly from your dashboard.
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=thekiwismarthome&repository=shopping-list-manager&category=integration)
|
---
|
||||||
|
|
||||||
Click the button above or manually add the custom repository in HACS.
|
## Features
|
||||||
Restart HA
|
|
||||||
|
|
||||||
### 2. Add the Card Resource
|
- 📋 Manage shopping list items from Home Assistant
|
||||||
|
- 🔌 WebSocket-based backend (no polling entities)
|
||||||
|
- 🖥️ Custom Lovelace card
|
||||||
|
- ⚙️ UI-based configuration (Config Flow)
|
||||||
|
- 🚀 Compatible with Home Assistant **2024.8+**
|
||||||
|
|
||||||
After installing via HACS, add the frontend resource:
|
---
|
||||||
|
|
||||||
1. Go to Settings → Dashboards
|
## 1. Installation (HACS)
|
||||||
2. Click ⋮ (three dots, top right) → Resources
|
|
||||||
3. Click "+ Add Resource"
|
|
||||||
4. URL: `/local/community/shopping-list-manager/shopping_list_card.js`
|
|
||||||
5. Resource type: JavaScript Module
|
|
||||||
6. Click "Create"
|
|
||||||
|
|
||||||
### 3. Add the Integration
|
[](
|
||||||
|
https://my.home-assistant.io/redirect/hacs_repository/?owner=thekiwismarthome&repository=shopping-list-manager&category=integration
|
||||||
|
)
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/config_flow_start/?domain=shopping_list_manager)
|
Click the button above **or** follow the manual steps below.
|
||||||
|
|
||||||
Click the button above or manually add via Settings → Devices & Services.
|
1. Open **HACS**
|
||||||
|
2. Go to **Integrations**
|
||||||
|
3. Click **⋮ → Custom repositories**
|
||||||
|
4. Add this repository:
|
||||||
|
- **Repository:** `https://github.com/thekiwismarthome/shopping-list-manager`
|
||||||
|
- **Category:** Integration
|
||||||
|
5. Install **Shopping List Manager**
|
||||||
|
6. **Restart Home Assistant**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Install the Lovelace Card Resource (Required)
|
||||||
|
|
||||||
|
The Lovelace card JavaScript file is included with the integration, but **must be copied manually** to the `www` directory so Home Assistant can load it.
|
||||||
|
|
||||||
|
### Step 1: Copy the card file
|
||||||
|
|
||||||
|
Run the following command (via SSH, Terminal add-on, or container shell):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /config/www/community/shopping_list_card && \
|
||||||
|
cp /config/custom_components/shopping_list_manager/frontend/shopping_list_card.js \
|
||||||
|
/config/www/community/shopping_list_card/shopping_list_card.js
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: Add the resource to Home Assistant
|
||||||
|
|
||||||
|
1. Go to **Settings → Dashboards**
|
||||||
|
2. Click **⋮ (top right) → Resources**
|
||||||
|
3. Click **Add Resource**
|
||||||
|
4. Enter:
|
||||||
|
|
||||||
|
```text
|
||||||
|
URL: /local/community/shopping_list_card/shopping_list_card.js
|
||||||
|
Type: JavaScript Module
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Click **Create**
|
||||||
|
6. Refresh your browser (**Ctrl + F5**)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Add the Integration
|
||||||
|
|
||||||
|
[](
|
||||||
|
https://my.home-assistant.io/redirect/config_flow_start/?domain=shopping_list_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
Click the button above **or** add it manually:
|
||||||
|
|
||||||
|
1. Go to **Settings → Devices & Services**
|
||||||
|
2. Click **Add Integration**
|
||||||
|
3. Search for **Shopping List Manager**
|
||||||
|
4. Follow the setup steps
|
||||||
|
|
||||||
|
No YAML configuration is required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Add the Card to a Dashboard
|
||||||
|
|
||||||
|
Add a **Manual** card to your dashboard and use the following YAML:
|
||||||
|
|
||||||
### 4. Add Card to Dashboard
|
|
||||||
```yaml
|
```yaml
|
||||||
type: custom:shopping-list-card
|
type: custom:shopping-list-card
|
||||||
title: Shopping List
|
title: Shopping List
|
||||||
list_id: groceries
|
list_id: groceries
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use the **⚙️ cog button** in the card to configure additional settings.
|
||||||
|
|
||||||
Use the ⚙️ cog button in the card to configure settings.
|
---
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
- HACS updates will update the **integration**
|
||||||
|
- If the Lovelace card JavaScript changes in a future release, you must **repeat the copy command** above
|
||||||
|
|
||||||
|
This is expected behavior for single-repository integrations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Compatibility Notes
|
||||||
|
|
||||||
|
- Designed for **Home Assistant 2024.8+**
|
||||||
|
- Uses WebSocket APIs
|
||||||
|
- Fully compatible with the **Services → Actions** change introduced in Home Assistant 2024.8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If the card does not load:
|
||||||
|
|
||||||
|
1. Ensure Home Assistant was restarted after installation
|
||||||
|
2. Verify the file exists at:
|
||||||
|
|
||||||
|
```
|
||||||
|
/config/www/community/shopping_list_card/shopping_list_card.js
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Confirm the resource URL is correct
|
||||||
|
4. Perform a hard browser refresh (**Ctrl + F5**)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|||||||
+83
-272
@@ -1,8 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Shopping List Manager - Custom Lovelace Card
|
* Shopping List Manager - Custom Lovelace Card
|
||||||
*
|
|
||||||
* Uses proper Home Assistant WebSocket API
|
|
||||||
* Version: 2.1.0 - Fixed syntax errors
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Category definitions
|
// Category definitions
|
||||||
@@ -112,16 +109,53 @@ class ShoppingListCard extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setConfig(config) {
|
setConfig(config) {
|
||||||
this._config = { ...config }; // shallow-copy: HA 2026+ freezes the original
|
if (!config || typeof config !== 'object') {
|
||||||
// Derive a unique settings key for THIS card instance so each card on
|
throw new Error('Invalid configuration');
|
||||||
// the dashboard keeps its own independent settings in localStorage.
|
}
|
||||||
// Set `card_id` in YAML for an explicit stable label; otherwise title is used.
|
|
||||||
const id = (config.card_id || config.title || 'shopping_list').toString().trim().toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
// Normalize + freeze config shape here
|
||||||
this._settingsKey = `shopping_list_settings_${id}`;
|
this._config = {
|
||||||
// Re-load settings now that we have the correct key
|
title: config.title ?? 'Shopping List',
|
||||||
this._settings = this._loadSettings();
|
card_id: config.card_id,
|
||||||
|
products_per_row: config.products_per_row ?? 'auto',
|
||||||
|
layout: config.layout ?? 'grid',
|
||||||
|
haptics: config.haptics ?? 'medium',
|
||||||
|
hide_completed: !!config.hide_completed,
|
||||||
|
compact_headers: !!config.compact_headers
|
||||||
|
};
|
||||||
|
|
||||||
|
// Derive a stable per-card storage key
|
||||||
|
const idSource =
|
||||||
|
this._config.card_id ||
|
||||||
|
this._config.title ||
|
||||||
|
'shopping_list';
|
||||||
|
|
||||||
|
const id = idSource
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9_]/g, '_');
|
||||||
|
|
||||||
|
const newSettingsKey = `shopping_list_settings_${id}`;
|
||||||
|
|
||||||
|
// If the card_id / title changed, reload settings
|
||||||
|
if (this._settingsKey !== newSettingsKey) {
|
||||||
|
this._settingsKey = newSettingsKey;
|
||||||
|
this._settings = this._loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// First-time init
|
||||||
|
if (!this._settings) {
|
||||||
|
this._settings = this._loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger a re-render if already attached
|
||||||
|
if (this.isConnected) {
|
||||||
|
this._render?.();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load data using Home Assistant's connection.sendMessagePromise
|
* Load data using Home Assistant's connection.sendMessagePromise
|
||||||
*/
|
*/
|
||||||
@@ -1110,15 +1144,17 @@ class ShoppingListCard extends HTMLElement {
|
|||||||
* Fuzzy search - matches even with typos or partial matches
|
* Fuzzy search - matches even with typos or partial matches
|
||||||
*/
|
*/
|
||||||
_fuzzyMatch(text, query) {
|
_fuzzyMatch(text, query) {
|
||||||
text = text.toLowerCase();
|
if (!text || !query) return false;
|
||||||
query = query.toLowerCase();
|
|
||||||
|
text = String(text).toLowerCase();
|
||||||
|
query = String(query).toLowerCase();
|
||||||
|
|
||||||
// Exact match or substring
|
// Exact match or substring
|
||||||
if (text.includes(query)) {
|
if (text.includes(query)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fuzzy match - allows for missing characters
|
// Fuzzy match - characters in order
|
||||||
let queryIndex = 0;
|
let queryIndex = 0;
|
||||||
for (let i = 0; i < text.length && queryIndex < query.length; i++) {
|
for (let i = 0; i < text.length && queryIndex < query.length; i++) {
|
||||||
if (text[i] === query[queryIndex]) {
|
if (text[i] === query[queryIndex]) {
|
||||||
@@ -1128,6 +1164,7 @@ class ShoppingListCard extends HTMLElement {
|
|||||||
return queryIndex === query.length;
|
return queryIndex === query.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter products by search and split into active/inactive
|
* Filter products by search and split into active/inactive
|
||||||
*/
|
*/
|
||||||
@@ -1138,7 +1175,8 @@ class ShoppingListCard extends HTMLElement {
|
|||||||
if (this._searchQuery) {
|
if (this._searchQuery) {
|
||||||
const query = this._searchQuery.toLowerCase();
|
const query = this._searchQuery.toLowerCase();
|
||||||
products = products.filter(product =>
|
products = products.filter(product =>
|
||||||
this._fuzzyMatch(product.name, query)
|
this._fuzzyMatch(product.name, query) ||
|
||||||
|
this._fuzzyMatch(product.category, query)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1185,6 +1223,8 @@ class ShoppingListCard extends HTMLElement {
|
|||||||
*/
|
*/
|
||||||
_getInactiveProducts() {
|
_getInactiveProducts() {
|
||||||
const filtered = this._getFilteredProducts();
|
const filtered = this._getFilteredProducts();
|
||||||
|
|
||||||
|
// 🔑 When searching, show ALL inactive matches (including recently used)
|
||||||
return filtered.filter(product => {
|
return filtered.filter(product => {
|
||||||
const qty = this._activeList[product.key]?.qty || 0;
|
const qty = this._activeList[product.key]?.qty || 0;
|
||||||
return qty === 0;
|
return qty === 0;
|
||||||
@@ -1660,22 +1700,36 @@ class ShoppingListCard extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show inactive products in "Recently Used" section (unless hidden)
|
// Show inactive products in "Recently Used" section (unless hidden)
|
||||||
if (inactiveProducts.length > 0 && !this._searchQuery && !this._settings.hideCompleted) {
|
if (inactiveProducts.length > 0 && !this._settings.hideCompleted) {
|
||||||
html += `
|
if (this._searchQuery) {
|
||||||
<div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--divider-color);">
|
// During search: just show matches, no header
|
||||||
<div style="font-size: 12px; color: var(--secondary-text-color); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500;">
|
const containerClass =
|
||||||
Recently Used
|
this._settings.layout === 'list' ? 'product-list' : 'product-grid';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="${containerClass}">
|
||||||
|
${inactiveProducts.map(p => this._renderProductTile(p)).join('')}
|
||||||
</div>
|
</div>
|
||||||
${this._sortBy === 'category' ?
|
`;
|
||||||
this._renderProductsByCategory(inactiveProducts, true) :
|
} else {
|
||||||
`<div class="${this._settings.layout === 'list' ? 'product-list' : 'product-grid'}">
|
// Normal Recently Used section
|
||||||
${inactiveProducts.map(product => this._renderProductTile(product)).join('')}
|
html += `
|
||||||
</div>`
|
<div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--divider-color);">
|
||||||
}
|
<div style="font-size: 12px; color: var(--secondary-text-color); margin-bottom: 12px; text-transform: uppercase;">
|
||||||
</div>
|
Recently Used
|
||||||
`;
|
</div>
|
||||||
|
${this._sortBy === 'category'
|
||||||
|
? this._renderProductsByCategory(inactiveProducts, true)
|
||||||
|
: `<div class="${this._settings.layout === 'list' ? 'product-list' : 'product-grid'}">
|
||||||
|
${inactiveProducts.map(p => this._renderProductTile(p)).join('')}
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Empty state
|
// Empty state
|
||||||
if (activeProducts.length === 0 && inactiveProducts.length === 0) {
|
if (activeProducts.length === 0 && inactiveProducts.length === 0) {
|
||||||
html += `
|
html += `
|
||||||
@@ -1940,257 +1994,14 @@ class ShoppingListCard extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily disabled GUI editor - use YAML configuration
|
|
||||||
// static getConfigElement() {
|
|
||||||
// return document.createElement('shopping-list-card-editor');
|
|
||||||
// }
|
|
||||||
|
|
||||||
static getStubConfig() {
|
|
||||||
return { title: 'Shopping List' };
|
|
||||||
}
|
|
||||||
|
|
||||||
getCardSize() {
|
getCardSize() {
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── GUI Config Editor ─────────────────────────────────────────
|
|
||||||
// This is the standard HA card-editor pattern. HA renders this
|
|
||||||
// element inside its "configure card" panel automatically when the
|
|
||||||
// card exposes a static getConfigElement() method.
|
|
||||||
class ShoppingListCardEditor extends HTMLElement {
|
|
||||||
setConfig(config) {
|
|
||||||
console.log('[Editor] setConfig called, _rendered:', this._rendered, 'config:', config);
|
|
||||||
this._config = { ...(config || {}) }; // shallow-copy: HA 2026+ freezes the original
|
|
||||||
if (!this._rendered) {
|
|
||||||
console.log('[Editor] First render, calling _render()');
|
|
||||||
this._render(); // only render once, then STOP
|
|
||||||
} else {
|
|
||||||
console.log('[Editor] Already rendered, ignoring setConfig');
|
|
||||||
}
|
|
||||||
// After first render, ignore setConfig calls - the updateConfig closure
|
|
||||||
// already handles live updates by mutating this._config directly
|
|
||||||
}
|
|
||||||
|
|
||||||
set hass(hass) {
|
|
||||||
console.log('[Editor] hass setter called, _rendered:', this._rendered);
|
|
||||||
this._hass = hass;
|
|
||||||
if (!this._rendered) this._render();
|
|
||||||
}
|
|
||||||
|
|
||||||
_render() {
|
|
||||||
if (!this._config) return; // nothing to render yet
|
|
||||||
|
|
||||||
// Attach shadow DOM once, reuse on subsequent renders
|
|
||||||
if (!this.shadowRoot) {
|
|
||||||
this.attachShadow({ mode: 'open' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = this._config.title || '';
|
|
||||||
const productsPerRow = String(this._config.products_per_row || '3');
|
|
||||||
const layout = this._config.layout || 'grid';
|
|
||||||
const haptics = this._config.haptics || 'medium';
|
|
||||||
const hideCompleted = !!this._config.hide_completed;
|
|
||||||
const compactHeaders = !!this._config.compact_headers;
|
|
||||||
|
|
||||||
this.shadowRoot.innerHTML = `
|
|
||||||
<style>
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
ha-text-field, ha-select {
|
|
||||||
display: block;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
.toggle-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 0;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.toggle-row + .toggle-row {
|
|
||||||
border-top: 1px solid var(--divider-color);
|
|
||||||
}
|
|
||||||
.toggle-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.toggle-label {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
.toggle-hint {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
.section-divider {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid var(--divider-color);
|
|
||||||
margin: 8px 0 16px 0;
|
|
||||||
}
|
|
||||||
.code-editor-link {
|
|
||||||
display: block;
|
|
||||||
margin-top: 16px;
|
|
||||||
padding: 12px 0 0 0;
|
|
||||||
border-top: 1px solid var(--divider-color);
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--primary-color);
|
|
||||||
cursor: pointer;
|
|
||||||
background: none;
|
|
||||||
border-left: none;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: none;
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.code-editor-link:hover { text-decoration: underline; }
|
|
||||||
.yaml-block {
|
|
||||||
background: var(--card-background-color);
|
|
||||||
border: 1px solid var(--divider-color);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 12px;
|
|
||||||
margin-top: 8px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
white-space: pre;
|
|
||||||
overflow-x: auto;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<div class="row">
|
|
||||||
<ha-text-field id="ed-title" label="Title" placeholder="Shopping List"></ha-text-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Products Per Row -->
|
|
||||||
<div class="row">
|
|
||||||
<ha-select id="ed-perrow" label="Products Per Row">
|
|
||||||
<mwc-list-item value="auto">Auto</mwc-list-item>
|
|
||||||
<mwc-list-item value="2">2</mwc-list-item>
|
|
||||||
<mwc-list-item value="3">3</mwc-list-item>
|
|
||||||
<mwc-list-item value="4">4</mwc-list-item>
|
|
||||||
<mwc-list-item value="5">5</mwc-list-item>
|
|
||||||
</ha-select>
|
|
||||||
|
|
||||||
<!-- Layout -->
|
|
||||||
<ha-select id="ed-layout" label="Layout">
|
|
||||||
<mwc-list-item value="grid">Grid</mwc-list-item>
|
|
||||||
<mwc-list-item value="list">List</mwc-list-item>
|
|
||||||
</ha-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Haptic Feedback -->
|
|
||||||
<div class="row">
|
|
||||||
<ha-select id="ed-haptics" label="Haptic Feedback">
|
|
||||||
<mwc-list-item value="off">Off</mwc-list-item>
|
|
||||||
<mwc-list-item value="low">Low</mwc-list-item>
|
|
||||||
<mwc-list-item value="medium">Medium</mwc-list-item>
|
|
||||||
<mwc-list-item value="high">High</mwc-list-item>
|
|
||||||
</ha-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="section-divider">
|
|
||||||
|
|
||||||
<!-- Toggle: Hide completed items -->
|
|
||||||
<div class="toggle-row">
|
|
||||||
<div class="toggle-text">
|
|
||||||
<span class="toggle-label">Hide completed items</span>
|
|
||||||
<span class="toggle-hint">Hides the "Recently Used" section showing products not currently on the list.</span>
|
|
||||||
</div>
|
|
||||||
<ha-switch id="ed-hide-completed"></ha-switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toggle: Compact headers -->
|
|
||||||
<div class="toggle-row">
|
|
||||||
<div class="toggle-text">
|
|
||||||
<span class="toggle-label">Compact headers</span>
|
|
||||||
<span class="toggle-hint">Show category names only (no emoji)</span>
|
|
||||||
</div>
|
|
||||||
<ha-switch id="ed-hide-headers"></ha-switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Show/hide YAML preview -->
|
|
||||||
<button class="code-editor-link" id="ed-code-toggle">
|
|
||||||
${this._showCodeEditor ? 'Hide code editor' : 'Show code editor'}
|
|
||||||
</button>
|
|
||||||
${this._showCodeEditor ? `<div class="yaml-block">${this._toYaml()}</div>` : ''}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// ── Set values imperatively (reliable with shadow DOM + HA components) ──
|
|
||||||
const titleEl = this.shadowRoot.querySelector('#ed-title');
|
|
||||||
const perrowEl = this.shadowRoot.querySelector('#ed-perrow');
|
|
||||||
const layoutEl = this.shadowRoot.querySelector('#ed-layout');
|
|
||||||
const hapticsEl = this.shadowRoot.querySelector('#ed-haptics');
|
|
||||||
const hideCompletedEl = this.shadowRoot.querySelector('#ed-hide-completed');
|
|
||||||
const hideHeadersEl = this.shadowRoot.querySelector('#ed-hide-headers');
|
|
||||||
|
|
||||||
titleEl.value = title;
|
|
||||||
perrowEl.value = productsPerRow;
|
|
||||||
layoutEl.value = layout;
|
|
||||||
hapticsEl.value = haptics;
|
|
||||||
hideCompletedEl.checked = hideCompleted;
|
|
||||||
hideHeadersEl.checked = compactHeaders;
|
|
||||||
|
|
||||||
// ── Single updateConfig closure — mirrors your working pattern ──
|
|
||||||
const updateConfig = () => {
|
|
||||||
console.log('[Editor] updateConfig called');
|
|
||||||
this._config = {
|
|
||||||
...this._config,
|
|
||||||
title: titleEl.value || 'Shopping List',
|
|
||||||
products_per_row: perrowEl.value,
|
|
||||||
layout: layoutEl.value,
|
|
||||||
haptics: hapticsEl.value,
|
|
||||||
hide_completed: hideCompletedEl.checked,
|
|
||||||
compact_headers: hideHeadersEl.checked
|
|
||||||
};
|
|
||||||
console.log('[Editor] Dispatching config-changed:', this._config);
|
|
||||||
this.dispatchEvent(new CustomEvent('config-changed', {
|
|
||||||
detail: { config: this._config },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
titleEl.addEventListener('input', updateConfig);
|
|
||||||
perrowEl.addEventListener('value-changed', updateConfig);
|
|
||||||
layoutEl.addEventListener('value-changed', updateConfig);
|
|
||||||
hapticsEl.addEventListener('value-changed', updateConfig);
|
|
||||||
hideCompletedEl.addEventListener('checked-changed', updateConfig);
|
|
||||||
hideHeadersEl.addEventListener('checked-changed', updateConfig);
|
|
||||||
|
|
||||||
this.shadowRoot.querySelector('#ed-code-toggle').addEventListener('click', () => {
|
|
||||||
this._showCodeEditor = !this._showCodeEditor;
|
|
||||||
this._render();
|
|
||||||
});
|
|
||||||
|
|
||||||
this._rendered = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_toYaml() {
|
|
||||||
const lines = ['type: custom:shopping-list-card'];
|
|
||||||
if (this._config.title) lines.push('title: ' + this._config.title);
|
|
||||||
if (this._config.products_per_row) lines.push('products_per_row: ' + this._config.products_per_row);
|
|
||||||
if (this._config.layout) lines.push('layout: ' + this._config.layout);
|
|
||||||
if (this._config.haptics) lines.push('haptics: ' + this._config.haptics);
|
|
||||||
if (this._config.hide_completed) lines.push('hide_completed: true');
|
|
||||||
if (this._config.compact_headers) lines.push('compact_headers: true');
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('shopping-list-card-editor', ShoppingListCardEditor);
|
|
||||||
customElements.define('shopping-list-card', ShoppingListCard);
|
customElements.define('shopping-list-card', ShoppingListCard);
|
||||||
|
|
||||||
window.customCards = window.customCards || [];
|
window.customCards = window.customCards || [];
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "shopping_list_manager",
|
"domain": "shopping_list_manager",
|
||||||
"name": "Shopping List Manager",
|
"name": "Shopping List Manager",
|
||||||
"version": "1.0.0",
|
"version": "1.4.0",
|
||||||
"documentation": "https://github.com/thekiwismarthome/shopping-list-manager",
|
"documentation": "https://github.com/thekiwismarthome/shopping-list-manager",
|
||||||
"issue_tracker": "https://github.com/thekiwismarthome/shopping-list-manager/issues",
|
"issue_tracker": "https://github.com/thekiwismarthome/shopping-list-manager/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
|
|||||||
Reference in New Issue
Block a user