Update visual editors
This commit is contained in:
parent
b06d43b883
commit
af7d27c160
|
|
@ -52,17 +52,12 @@ class CookCliCard extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Éditeur de configuration automatique intégré à Home Assistant.
|
* Éditeur visuel : élément dédié (voir CookCliCardEditor plus bas) plutôt
|
||||||
* HA utilise ce schéma pour générer le formulaire visuel.
|
* que getConfigForm(), dont le schéma est statique et n'a pas accès à
|
||||||
*/
|
* hass — or la liste des dashboards disponibles en dépend.
|
||||||
static getConfigForm() {
|
*/
|
||||||
return {
|
static getConfigElement() {
|
||||||
schema: [
|
return document.createElement("cookcli-card-editor");
|
||||||
{ name: "title", selector: { text: {} }, label: "Titre" },
|
|
||||||
{ name: "dashboard_path", selector: { text: {} }, label: "Chemin du dashboard", helper: "Laisser vide pour le dashboard courant" },
|
|
||||||
{ name: "entry_id", selector: { text: {} }, label: "Entry ID", helper: "Optionnel — si plusieurs serveurs CookCLI" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set hass(hass) {
|
set hass(hass) {
|
||||||
|
|
@ -248,7 +243,135 @@ class CookCliCard extends HTMLElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Éditeur visuel de la carte (ha-form + sélecteurs natifs).
|
||||||
|
*
|
||||||
|
* "Chemin du dashboard" propose les dashboards Lovelace du serveur, lus dans
|
||||||
|
* hass.panels (accessible à tous les utilisateurs, contrairement à
|
||||||
|
* lovelace/dashboards/list qui demande les droits admin). custom_value reste
|
||||||
|
* activé pour pouvoir saisir un chemin qui n'apparaît pas dans la liste.
|
||||||
|
*/
|
||||||
|
const COOKCLI_CARD_EDITOR_FIELDS = ["title", "dashboard_path", "entry_id"];
|
||||||
|
|
||||||
|
const COOKCLI_CARD_EDITOR_LABELS = {
|
||||||
|
title: "Titre",
|
||||||
|
dashboard_path: "Dashboard des recettes",
|
||||||
|
entry_id: "Serveur CookCLI",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COOKCLI_CARD_EDITOR_HELPERS = {
|
||||||
|
title: "Titre de la carte. Par défaut : « Recettes ».",
|
||||||
|
dashboard_path:
|
||||||
|
"Seulement si la carte n'est pas sur le dashboard qui contient les vues recettes. Vide = dashboard courant.",
|
||||||
|
entry_id:
|
||||||
|
"Seulement si tu as plusieurs serveurs CookCLI. Vide = le serveur par défaut.",
|
||||||
|
};
|
||||||
|
|
||||||
|
class CookCliCardEditor extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._config = {};
|
||||||
|
this._hass = undefined;
|
||||||
|
this._form = null;
|
||||||
|
this._optionsSignature = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(config) {
|
||||||
|
this._config = config || {};
|
||||||
|
this._update();
|
||||||
|
}
|
||||||
|
|
||||||
|
set hass(hass) {
|
||||||
|
this._hass = hass;
|
||||||
|
this._update();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this._ensureForm();
|
||||||
|
this._update();
|
||||||
|
}
|
||||||
|
|
||||||
|
_dashboardOptions() {
|
||||||
|
const panels = (this._hass && this._hass.panels) || {};
|
||||||
|
return Object.values(panels)
|
||||||
|
.filter((p) => p.component_name === "lovelace" && p.url_path)
|
||||||
|
.map((p) => ({
|
||||||
|
value: p.url_path,
|
||||||
|
label: p.title ? `${p.title} (${p.url_path})` : p.url_path,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildSchema(options) {
|
||||||
|
return [
|
||||||
|
{ name: "title", selector: { text: {} } },
|
||||||
|
{
|
||||||
|
name: "dashboard_path",
|
||||||
|
selector: { select: { options, custom_value: true, mode: "dropdown" } },
|
||||||
|
},
|
||||||
|
{ name: "entry_id", selector: { config_entry: { integration: "cookcli" } } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureForm() {
|
||||||
|
if (this._form) return;
|
||||||
|
const form = document.createElement("ha-form");
|
||||||
|
const options = this._dashboardOptions();
|
||||||
|
this._optionsSignature = JSON.stringify(options);
|
||||||
|
form.schema = this._buildSchema(options);
|
||||||
|
form.computeLabel = (schema) => COOKCLI_CARD_EDITOR_LABELS[schema.name] || schema.name;
|
||||||
|
form.computeHelper = (schema) => COOKCLI_CARD_EDITOR_HELPERS[schema.name] || "";
|
||||||
|
form.addEventListener("value-changed", (ev) => this._valueChanged(ev));
|
||||||
|
this.appendChild(form);
|
||||||
|
this._form = form;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formulaire créé une fois ; on ne met à jour que ses propriétés. Le schéma
|
||||||
|
// n'est reconstruit que si la liste des dashboards a changé (hass, lui,
|
||||||
|
// change en permanence).
|
||||||
|
_update() {
|
||||||
|
if (!this._form) return;
|
||||||
|
if (this._hass) {
|
||||||
|
this._form.hass = this._hass;
|
||||||
|
const options = this._dashboardOptions();
|
||||||
|
const signature = JSON.stringify(options);
|
||||||
|
if (signature !== this._optionsSignature) {
|
||||||
|
this._optionsSignature = signature;
|
||||||
|
this._form.schema = this._buildSchema(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data = {};
|
||||||
|
for (const name of COOKCLI_CARD_EDITOR_FIELDS) {
|
||||||
|
if (this._config[name] !== undefined) data[name] = this._config[name];
|
||||||
|
}
|
||||||
|
this._form.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
_valueChanged(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const value = ev.detail.value || {};
|
||||||
|
|
||||||
|
// Garde `type` et toute clé inconnue ; un champ vidé est supprimé.
|
||||||
|
const config = { ...this._config };
|
||||||
|
for (const name of COOKCLI_CARD_EDITOR_FIELDS) {
|
||||||
|
const v = value[name];
|
||||||
|
if (v === undefined || v === null || v === "") delete config[name];
|
||||||
|
else config[name] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._config = config;
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("config-changed", {
|
||||||
|
detail: { config },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
customElements.define("cookcli-card", CookCliCard);
|
customElements.define("cookcli-card", CookCliCard);
|
||||||
|
customElements.define("cookcli-card-editor", CookCliCardEditor);
|
||||||
|
|
||||||
window.customCards = window.customCards || [];
|
window.customCards = window.customCards || [];
|
||||||
window.customCards.push({
|
window.customCards.push({
|
||||||
|
|
@ -256,4 +379,4 @@ window.customCards.push({
|
||||||
name: "CookCLI Recipes",
|
name: "CookCLI Recipes",
|
||||||
preview: false,
|
preview: false,
|
||||||
description: "Liste navigable de tes recettes CookCLI.",
|
description: "Liste navigable de tes recettes CookCLI.",
|
||||||
});
|
});
|
||||||
|
|
@ -41,7 +41,8 @@
|
||||||
* - custom:mod-card (card-mod) : force le ratio 30/70 des colonnes d'étape
|
* - custom:mod-card (card-mod) : force le ratio 30/70 des colonnes d'étape
|
||||||
* - custom:cookcli-card : la liste (voir cookcli-card.js)
|
* - custom:cookcli-card : la liste (voir cookcli-card.js)
|
||||||
*
|
*
|
||||||
* Configuration du tableau de bord (remplace le contenu YAML du dashboard) :
|
* Configuration du tableau de bord (éditable aussi via l'éditeur visuel,
|
||||||
|
* voir CookcliStrategyEditor ; sinon remplace le contenu YAML du dashboard) :
|
||||||
* strategy:
|
* strategy:
|
||||||
* type: custom:cookcli
|
* type: custom:cookcli
|
||||||
* title: Recettes
|
* title: Recettes
|
||||||
|
|
@ -405,77 +406,111 @@ class CookCliRecipeViewStrategy extends HTMLElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Éditeur visuel de la dashboard strategy (remplace l'édition YAML).
|
||||||
|
*
|
||||||
|
* Basé sur <ha-form> + sélecteurs natifs HA : mêmes composants, même rendu
|
||||||
|
* que les éditeurs des cartes officielles (thème, sélecteurs d'entités,
|
||||||
|
* traductions). Aucune dépendance Lit : on manipule directement l'élément.
|
||||||
|
*
|
||||||
|
* Contrat HA pour un éditeur de strategy :
|
||||||
|
* - setConfig(config) → reçoit la config courante
|
||||||
|
* - set hass(hass) → reçoit l'objet hass (mis à jour très souvent)
|
||||||
|
* - événement "config-changed" { detail: { config } } → sauvegarde
|
||||||
|
*/
|
||||||
|
const COOKCLI_EDITOR_SCHEMA = [
|
||||||
|
{ name: "title", selector: { text: {} } },
|
||||||
|
{ name: "timer_entity", selector: { entity: { domain: "timer" } } },
|
||||||
|
{ name: "step_entity", selector: { entity: { domain: "input_number" } } },
|
||||||
|
{ name: "entry_id", selector: { config_entry: { integration: "cookcli" } } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const COOKCLI_EDITOR_LABELS = {
|
||||||
|
title: "Titre du dashboard",
|
||||||
|
timer_entity: "Minuteur",
|
||||||
|
step_entity: "Étape en cours",
|
||||||
|
entry_id: "Serveur CookCLI",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COOKCLI_EDITOR_HELPERS = {
|
||||||
|
title: "Titre de la liste et du dashboard. Par défaut : « Recettes ».",
|
||||||
|
timer_entity:
|
||||||
|
"Aide « Minuteur » partagée entre toutes les recettes (ex. timer.recette_en_cours). Vide = pas de boutons de minuteur.",
|
||||||
|
step_entity:
|
||||||
|
"Aide « Nombre » qui pilote l'onglet actif (ex. input_number.recette_etape). Vide = pas de boutons Commencer / Précédent / Suivant.",
|
||||||
|
entry_id:
|
||||||
|
"Seulement si tu as plusieurs serveurs CookCLI. Vide = le serveur par défaut.",
|
||||||
|
};
|
||||||
|
|
||||||
class CookcliStrategyEditor extends HTMLElement {
|
class CookcliStrategyEditor extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._config = {};
|
||||||
|
this._hass = undefined;
|
||||||
|
this._form = null;
|
||||||
|
}
|
||||||
|
|
||||||
setConfig(config) {
|
setConfig(config) {
|
||||||
this._config = config;
|
this._config = config || {};
|
||||||
|
this._update();
|
||||||
}
|
}
|
||||||
|
|
||||||
set hass(hass) {
|
set hass(hass) {
|
||||||
this._hass = hass;
|
this._hass = hass;
|
||||||
|
this._update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appelé automatiquement quand l'élément est inséré dans le DOM
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this._render();
|
this._ensureForm();
|
||||||
|
this._update();
|
||||||
}
|
}
|
||||||
|
|
||||||
_render() {
|
_ensureForm() {
|
||||||
console.log("Editor render appelé", this._config, this._hass);
|
if (this._form) return;
|
||||||
|
const form = document.createElement("ha-form");
|
||||||
|
form.schema = COOKCLI_EDITOR_SCHEMA;
|
||||||
|
form.computeLabel = (schema) => COOKCLI_EDITOR_LABELS[schema.name] || schema.name;
|
||||||
|
form.computeHelper = (schema) => COOKCLI_EDITOR_HELPERS[schema.name] || "";
|
||||||
|
form.addEventListener("value-changed", (ev) => this._valueChanged(ev));
|
||||||
|
this.appendChild(form);
|
||||||
|
this._form = form;
|
||||||
|
}
|
||||||
|
|
||||||
// On attend d'avoir hass avant de rendre : ha-input en a besoin
|
// Le formulaire est créé une seule fois ; on ne met à jour que ses
|
||||||
// pour son thème et ses styles.
|
// propriétés (pas de re-rendu du DOM à chaque changement de hass, ce qui
|
||||||
if (!this._hass) {
|
// ferait perdre le focus pendant la saisie).
|
||||||
this.innerHTML = `<div style="padding:16px">Chargement…</div>`;
|
_update() {
|
||||||
return;
|
if (!this._form) return;
|
||||||
|
if (this._hass) this._form.hass = this._hass;
|
||||||
|
const data = {};
|
||||||
|
for (const { name } of COOKCLI_EDITOR_SCHEMA) {
|
||||||
|
if (this._config[name] !== undefined) data[name] = this._config[name];
|
||||||
|
}
|
||||||
|
this._form.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
_valueChanged(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const value = ev.detail.value || {};
|
||||||
|
|
||||||
|
// On part de la config existante (garde `type: custom:cookcli` et toute
|
||||||
|
// clé inconnue) et on ne touche qu'aux champs de l'éditeur. Un champ vidé
|
||||||
|
// est supprimé plutôt que stocké comme "".
|
||||||
|
const config = { ...this._config };
|
||||||
|
for (const { name } of COOKCLI_EDITOR_SCHEMA) {
|
||||||
|
const v = value[name];
|
||||||
|
if (v === undefined || v === null || v === "") delete config[name];
|
||||||
|
else config[name] = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
const c = this._config || {};
|
this._config = config;
|
||||||
|
this.dispatchEvent(
|
||||||
// Remplacer temporairement ha-textfield par des inputs natifs
|
new CustomEvent("config-changed", {
|
||||||
this.innerHTML = `
|
detail: { config },
|
||||||
<div style="padding: 16px; display: flex; flex-direction: column; gap: 16px;">
|
bubbles: true,
|
||||||
<ha-input
|
composed: true,
|
||||||
label="Titre du dashboard"
|
})
|
||||||
value="${c.title || ""}"
|
);
|
||||||
data-config-key="title"
|
|
||||||
helper="Titre affiché dans l'onglet"
|
|
||||||
style="width: 100%;"
|
|
||||||
></ha-input>
|
|
||||||
|
|
||||||
<ha-input
|
|
||||||
label="Entité minuteur"
|
|
||||||
value="${c.timer_entity || ""}"
|
|
||||||
data-config-key="timer_entity"
|
|
||||||
helper="Ex: timer.recette_en_cours"
|
|
||||||
style="width: 100%;"
|
|
||||||
></ha-input>
|
|
||||||
|
|
||||||
<ha-input
|
|
||||||
label="Entité étape"
|
|
||||||
value="${c.step_entity || ""}"
|
|
||||||
data-config-key="step_entity"
|
|
||||||
helper="Ex: input_number.recette_etape"
|
|
||||||
style="width: 100%;"
|
|
||||||
></ha-input>
|
|
||||||
|
|
||||||
<ha-input
|
|
||||||
label="Entry ID (optionnel)"
|
|
||||||
value="${c.entry_id || ""}"
|
|
||||||
data-config-key="entry_id"
|
|
||||||
helper="Laisser vide si un seul serveur CookCLI"
|
|
||||||
style="width: 100%;"
|
|
||||||
></ha-input>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Écouteurs natifs (change au lieu de input pour éviter les re-rendus intempestifs)
|
|
||||||
this.querySelectorAll("[data-config-key]").forEach((champ) => {
|
|
||||||
champ.addEventListener("change", (ev) => {
|
|
||||||
const cle = ev.target.dataset.configKey;
|
|
||||||
const nouvelleConfig = { ...this._config, [cle]: ev.target.value };
|
|
||||||
this._config = nouvelleConfig;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue