After an extensive search for a TODO list search code, which was unsuccessful, help was sought from Gemini, which provided functional code.
Since programming experience is limited, optimization of the code would be appreciated. The final version should be saved in www/todo_search/todo-search-card.js
Installation steps:
- Go to Settings > Dashboards.
- Click the three dots in the upper right corner and select Resources.
- Click Add Resource and enter the following path: /local/todo_search/todo-search-card.js
- Ensure the resource type is set to JavaScript Module.
- Restart Home Assistant.
- To use it, add a Manual Card to your dashboard with the following configuration:
type: custom:todo-search-card
todo-search-card.js:
class TodoSearchCard extends HTMLElement {
setConfig(config) {
this.config = config;
}
set hass(hass) {
const oldHass = this._hass;
this._hass = hass;
if (!this.content) {
this._createCard();
}
if (!oldHass || oldHass.states !== hass.states) {
this._loadAllTodos();
}
}
_createCard() {
this.attachShadow({ mode: “open” });
this.shadowRoot.innerHTML = <style> ha-card { padding: 16px; direction: ltr; } .search-container { position: relative; margin-bottom: 16px; } input { width: 100%; padding: 12px; padding-right: 40px; box-sizing: border-box; border-radius: 4px; border: 1px solid var(--divider-color); background: var(--card-background-color); color: var(--primary-text-color); } .clear-btn { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); cursor: pointer; color: var(--secondary-text-color); font-weight: bold; border: none; background: none; font-size: 18px; display: none; } .list-section { margin-bottom: 16px; border-bottom: 1px solid var(--divider-color); } .list-title { font-weight: bold; color: var(--accent-color); margin-bottom: 8px; } .item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-top: 1px solid var(--secondary-background-color); } .action-btn { background: var(--error-color); color: white; border: none; border-radius: 4px; padding: 4px 8px; cursor: pointer; } .no-results { color: var(--secondary-text-color); text-align: center; margin-top: 10px; } </style> <ha-card> <div class="search-container"> <input type="text" id="searchInput" placeholder="Search lists..."> <button class="clear-btn" id="clearBtn">×</button> </div> <div id="results"></div> </ha-card> ;
const input = this.shadowRoot.querySelector("#searchInput");
const clearBtn = this.shadowRoot.querySelector("#clearBtn");
this.content = this.shadowRoot.querySelector("#results");
input.addEventListener("input", (e) => {
this._searchTerm = e.target.value.toLowerCase();
clearBtn.style.display = this._searchTerm ? "block" : "none";
this._renderResults();
});
clearBtn.addEventListener("click", () => {
input.value = "";
this._searchTerm = "";
clearBtn.style.display = "none";
this._renderResults();
});
}
async _loadAllTodos() {
const todoEntities = Object.keys(this._hass.states).filter(eid => eid.startsWith(‘todo.’));
this.allTodos = {};
for (const entity of todoEntities) {
try {
const result = await this._hass.callWS({
type: "todo/item/list",
entity_id: entity,
});
this.allTodos[entity] = result.items;
} catch (e) {
console.error("Error loading todos for", entity, e);
}
}
this._renderResults();
}
_renderResults() {
if (!this.allTodos || !this.content) return;
const q = this._searchTerm || “”;
this.content.innerHTML = “”;
if (q.length === 0) return;
let foundAny = false;
for (const [entityId, items] of Object.entries(this.allTodos)) {
const filtered = items.filter(i => i.summary.toLowerCase().includes(q) && i.status === "needs_action");
if (filtered.length === 0) continue;
foundAny = true;
const section = document.createElement("div");
section.className = "list-section";
const friendlyName = this._hass.states[entityId].attributes.friendly_name || entityId;
section.innerHTML = `<div class="list-title">${friendlyName}</div>`;
filtered.forEach(item => {
const row = document.createElement("div");
row.className = "item";
row.innerHTML = `<span>${item.summary}</span>`;
const btn = document.createElement("button");
btn.className = "action-btn";
btn.textContent = "Done";
btn.onclick = async () => {
await this._hass.callService("todo", "update_item", {
entity_id: entityId,
item: item.uid || item.summary,
status: "completed"
});
this._loadAllTodos();
};
row.appendChild(btn);
section.appendChild(row);
});
this.content.appendChild(section);
}
if (!foundAny) {
const noResults = document.createElement("div");
noResults.className = "no-results";
noResults.textContent = `No results found for "${q}"`;
this.content.appendChild(noResults);
}
}
getCardSize() { return 5; }
}
customElements.define(“todo-search-card”, TodoSearchCard);