import json import db from ..widgets.widget_meta import widget_meta from ..widgets.layout_capable_widgets import LAYOUT_CAPABLE_WIDGETS from ..clause_list_codec import encode_clauses from .get_element import get_element def _hex_to_rgb(hex_color): h = (hex_color or "").lstrip("#") if len(h) != 6: return (0, 0, 0) try: return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) except ValueError: return (0, 0, 0) def _is_default_value(control, raw_value): """Compare la valeur reçue du formulaire à la valeur par défaut du réglage (en tenant compte de l'unité ajoutée pour les sliders), pour savoir si le champ a simplement été renvoyé tel quel sans être personnalisé.""" default = control.get("default", "") if control["type"] == "slider": unit = control.get("unit", "") default = f"{default}{unit}" if default not in (None, "") else "" return str(raw_value) == str(default) def save_element_controls(slug, element_id, form): """Lit, pour chaque réglage du widget de cet élément, la valeur envoyée par le panneau de propriétés (un seul formulaire, un champ par réglage) et met à jour content / attributs / style en conséquence.""" el = get_element(slug, element_id) if not el: return meta = widget_meta(el["widget"]) content = el["content"] attributes = dict(el["attributes"]) style = dict(el["style"]) toggle_groups = {} # propriété CSS -> ensemble des "tokens" cochés (ex: text-decoration) for control in meta.get("controls", []): ctype = control["type"] field_name = "ctrl_" + control["key"] if ctype == "toggle_token": _kind, name = control["target"].split(":", 1) toggle_groups.setdefault(name, set()) if form.get(field_name) is not None: toggle_groups[name].add(control["token"]) continue if ctype == "clause_list": # Nombre ILLIMITÉ de lignes (voir c_clause_list.py) : un champ # tableau par colonne ("field_name_champ[]"/"_operateur[]"/ # "_valeur[]"), reconstituées ici en une liste de clauses par # position (zip) — même principe que field_name[]/field_type[] # pour les champs d'un objet (routes/objects/parse_field_rows.py). champs = form.getlist(field_name + "_champ[]") operateurs = form.getlist(field_name + "_operateur[]") valeurs = form.getlist(field_name + "_valeur[]") clauses = [ {"champ": champ, "operateur": operateurs[i] if i < len(operateurs) else "egal", "valeur": valeurs[i] if i < len(valeurs) else ""} for i, champ in enumerate(champs) ] encoded = encode_clauses(clauses) if encoded: attributes[control["clauses_attr"]] = encoded else: attributes.pop(control["clauses_attr"], None) combinateur = form.get(field_name + "_combinateur") or "et" if combinateur == "ou": attributes[control["combinator_attr"]] = combinateur else: attributes.pop(control["combinator_attr"], None) continue if ctype == "preset": chosen = form.get(field_name, control.get("default")) opt = next((o for o in control["options"] if o[0] == chosen), None) if opt: for k, v in opt[2].items(): if v in (None, ""): style.pop(k, None) else: style[k] = v if ( control["key"] == "visibilite" and chosen == "visible" and el["widget"] not in LAYOUT_CAPABLE_WIDGETS ): # "Visible" ne touche volontairement pas "display" (voir # visibility_control.py) : sur un widget qui A un # réglage "Disposition interne" (LAYOUT_CONTROLS, # traité avant celui-ci sur le MÊME enregistrement, # voir widget_meta.py), ce contrôle réaffirme lui-même # un "display" non-"none" (flex/etc), donc "Visible" n'a # rien à faire. Mais un widget SANS ce contrôle (ex. # "superposition") n'a PERSONNE d'autre pour l'effacer : # un "display:none" laissé par un "Masqué" précédent (ou # par le réglage par défaut à la création, voir # default_style_for_widget.py) restait donc bloqué # masqué pour toujours, quel que soit le nombre de fois # où "Visible" était ensuite choisi. style.pop("display", None) continue if ctype == "scale": try: val = float(form.get(field_name, control.get("default", 1))) except (TypeError, ValueError): val = 1.0 if abs(val - 1) < 1e-6: style.pop("transform", None) else: style["transform"] = f"scale({val})" continue if ctype == "size": # Un seul champ CSS ("width"/"height") mais deux entrées de # formulaire (valeur + unité) — voir size_override_controls.py # pour pourquoi ce n'est PAS deux contrôles slider séparés. _kind, name = control["target"].split(":", 1) try: num = float(form.get(field_name + "_value") or 0) except (TypeError, ValueError): num = 0.0 unit = form.get(field_name + "_unit", "%") if unit not in ("px", "%"): unit = "%" if num <= 0: style.pop(name, None) else: num_str = str(int(num)) if num == int(num) else str(num) style[name] = f"{num_str}{unit}" continue if ctype == "shadow": # 6 entrées de formulaire (décalage X/Y, flou, étendue, couleur, # opacité — voir c_shadow.py) combinées en une seule valeur CSS # "box-shadow" (couleur+opacité fusionnées en rgba(), un # seul ne portant pas de canal alpha). _kind, name = control["target"].split(":", 1) defaults = control.get("default", {}) def _num(suffix, fallback): try: return float(form.get(field_name + suffix, fallback)) except (TypeError, ValueError): return fallback x = _num("_x", defaults.get("x", 0)) y = _num("_y", defaults.get("y", 4)) blur = _num("_blur", defaults.get("blur", 12)) spread = _num("_spread", defaults.get("spread", 0)) opacity = _num("_opacity", defaults.get("opacity", 0.4)) color = form.get(field_name + "_color") or defaults.get("color", "#000000") if x == 0 and y == 0 and blur == 0 and spread == 0: style.pop(name, None) else: r, g, b = _hex_to_rgb(color) def _px(n): return str(int(n)) if n == int(n) else str(n) style[name] = f"{_px(x)}px {_px(y)}px {_px(blur)}px {_px(spread)}px rgba({r}, {g}, {b}, {opacity})" continue target = control["target"] if ctype == "checkbox": checked = form.get(field_name) is not None raw_value = control["true_value"] if checked else control["false_value"] else: raw_value = form.get(field_name, control.get("default", "")) if ctype == "slider": unit = control.get("unit", "") raw_value = f"{raw_value}{unit}" if raw_value not in (None, "") else "" if target == "content": content = raw_value else: kind, name = target.split(":", 1) if kind == "class": # "class:xxx" : un jeton de classe CSS externe (ex. un # modificateur Bulma) parmi d'autres, chacun mémorisé sous sa # propre clé "_class_xxx" pour ne pas s'écraser entre eux — # voir _visible_attrs.py qui les rassemble avec la classe de # base du widget (fixed_attrs) au moment du rendu. store, name = attributes, "_class_" + name else: store = attributes if kind == "attr" else style if raw_value in (None, ""): store.pop(name, None) elif ctype != "checkbox" and name not in store and _is_default_value(control, raw_value): # Ce réglage n'a jamais été personnalisé (la clé est absente # du style/des attributs stockés) et la valeur reçue est # simplement celle par défaut affichée dans le champ (un # ou un slider a TOUJOURS une valeur à # l'écran, même quand rien n'a été choisi). Comme le # panneau de propriétés envoie TOUS les réglages du widget # en un seul formulaire, sauvegarder n'importe quel autre # champ (ex. changer la disposition) écrirait sinon cette # valeur par défaut "en dur" dans l'élément — ce qui faisait # apparaître par exemple une couleur de fond qui n'avait # jamais été demandée. On laisse donc ce réglage "non # défini" tant qu'il n'a pas été explicitement changé. pass else: store[name] = raw_value for prop, tokens in toggle_groups.items(): if tokens: style[prop] = " ".join(sorted(tokens)) else: style.pop(prop, None) conn = db.connect(slug) conn.execute( "UPDATE _screen_elements SET content = ?, attributes = ?, style = ? WHERE id = ?", (content, json.dumps(attributes), json.dumps(style), element_id), ) conn.commit() conn.close()