Add custom checklist card
This commit is contained in:
parent
faf9eed3c1
commit
a2aabbf8a9
|
|
@ -636,8 +636,151 @@ class CookCliCardEditor extends HTMLElement {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checklist d'ingrédients d'une étape (cases à cocher, état local).
|
||||
*
|
||||
* L'état coché est stocké dans localStorage du navigateur, par recette et par
|
||||
* étape (`storage_key`) : il n'est pas partagé entre appareils, ni lié à la
|
||||
* todo du Résumé. Config :
|
||||
* type: custom:cookcli-checklist-card
|
||||
* storage_key: chocolat-chaud.cook:2
|
||||
* items: [{ quantity: "200 ml", name: "lait" }, ...]
|
||||
*/
|
||||
class CookCliChecklistCard extends HTMLElement {
|
||||
setConfig(config) {
|
||||
if (!config || !Array.isArray(config.items)) {
|
||||
throw new Error("La carte a besoin d'une liste `items`.");
|
||||
}
|
||||
this._config = config;
|
||||
this._checked = this._load();
|
||||
|
||||
if (!this.shadowRoot) {
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot.addEventListener("change", (ev) => this._onChange(ev));
|
||||
this.shadowRoot.addEventListener("click", (ev) => this._onClick(ev));
|
||||
}
|
||||
this._render();
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return (this._config?.items?.length || 0) + 1;
|
||||
}
|
||||
|
||||
_storageKey() {
|
||||
return `cookcli-checklist:${this._config.storage_key || "default"}`;
|
||||
}
|
||||
|
||||
// Identifiant d'une ligne : index + libellé, pour rester correct si deux
|
||||
// lignes ont le même nom ou si la recette est modifiée.
|
||||
_itemId(item, index) {
|
||||
return `${index}:${item.quantity || ""} ${item.name || ""}`.trim();
|
||||
}
|
||||
|
||||
_load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this._storageKey());
|
||||
const parsed = raw ? JSON.parse(raw) : [];
|
||||
return new Set(Array.isArray(parsed) ? parsed : []);
|
||||
} catch (err) {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
_save() {
|
||||
try {
|
||||
localStorage.setItem(this._storageKey(), JSON.stringify([...this._checked]));
|
||||
} catch (err) {
|
||||
// localStorage indisponible : l'état reste valable jusqu'au prochain rendu.
|
||||
}
|
||||
}
|
||||
|
||||
_onChange(ev) {
|
||||
const input = ev.target.closest("input[data-id]");
|
||||
if (!input) return;
|
||||
if (input.checked) this._checked.add(input.dataset.id);
|
||||
else this._checked.delete(input.dataset.id);
|
||||
this._save();
|
||||
this._render();
|
||||
}
|
||||
|
||||
_onClick(ev) {
|
||||
if (ev.target.closest("[data-reset]")) {
|
||||
this._checked.clear();
|
||||
this._save();
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
|
||||
_escape(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str ?? "";
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
_render() {
|
||||
if (!this.shadowRoot) return;
|
||||
const items = this._config.items;
|
||||
const anyChecked = items.some((item, i) => this._checked.has(this._itemId(item, i)));
|
||||
|
||||
const rows = items
|
||||
.map((item, i) => {
|
||||
const id = this._itemId(item, i);
|
||||
const checked = this._checked.has(id);
|
||||
return `
|
||||
<label class="row ${checked ? "done" : ""}">
|
||||
<input type="checkbox" data-id="${this._escape(id)}" ${checked ? "checked" : ""}>
|
||||
<span class="label">
|
||||
${item.quantity ? `<strong>${this._escape(item.quantity)}</strong> ` : ""}${this._escape(item.name)}
|
||||
</span>
|
||||
</label>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
ha-card { padding: 8px 16px; }
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
cursor: pointer;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.row input {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
accent-color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.row.done .label {
|
||||
text-decoration: line-through;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.reset {
|
||||
display: ${anyChecked ? "block" : "none"};
|
||||
margin-left: auto;
|
||||
padding: 4px 0 0;
|
||||
background: none;
|
||||
border: none;
|
||||
font: inherit;
|
||||
font-size: 0.85em;
|
||||
color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<ha-card>
|
||||
${rows}
|
||||
<button class="reset" data-reset>Tout décocher</button>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cookcli-card", CookCliCard);
|
||||
customElements.define("cookcli-card-editor", CookCliCardEditor);
|
||||
customElements.define("cookcli-checklist-card", CookCliChecklistCard);
|
||||
|
||||
window.customCards = window.customCards || [];
|
||||
window.customCards.push({
|
||||
|
|
|
|||
|
|
@ -388,13 +388,17 @@ class CookCliRecipeViewStrategy extends HTMLElement {
|
|||
const leftCards = [];
|
||||
const stepIngredients = (step.items || []).filter((item) => item.type === "ingredient");
|
||||
if (stepIngredients.length) {
|
||||
const lines = stepIngredients.map((item) => {
|
||||
const qty = item.quantity ?? ""
|
||||
? `${item.quantity.value ?? ""} ${item.quantity.unit ?? ""}`.trim()
|
||||
: "";
|
||||
return `- ${qty ? `**${qty}** ` : ""}${item.name ?? ""}`;
|
||||
leftCards.push({
|
||||
type: "custom:cookcli-checklist-card",
|
||||
// recette + étape : l'état coché est propre à chaque étape de chaque recette
|
||||
storage_key: `${config.path}:${tabIndex}`,
|
||||
items: stepIngredients.map((item) => ({
|
||||
quantity: item.quantity
|
||||
? `${item.quantity.value ?? ""} ${item.quantity.unit ?? ""}`.trim()
|
||||
: "",
|
||||
name: item.name ?? "",
|
||||
})),
|
||||
});
|
||||
leftCards.push({ type: "markdown", content: lines.join("\n") });
|
||||
}
|
||||
|
||||
if (config.timer_entity) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue