import React, { useEffect, useRef, useState } from 'react'; import { widgetDefaults, isObj, clone } from '../engine/index.js'; import { CFG, getState, useStore, setSetting, setStyle, moveRepeaterItem, getPath } from './store.js'; import { BPS, BP_IDS } from './bridge.js'; import { I, Btn, Field, Seg, Switch, UnitInput, cx, Section, tr } from './ui.jsx'; import { ColorControl } from './color.jsx'; import { MediaControl, GalleryControl, IconControl, LinkControl, FontControl, PostsPicker, TermsPicker, TemplatePicker, MenuSelect, PopupSelect, DynButton, DynChip, } from './pickers.jsx'; import { __ } from './i18n.js'; const bpLabel = (bp) => BPS.find((b) => b.id === bp)?.label || 'Desktop'; const isResp = (v) => isObj(v) && ('d' in v || 'l' in v || 't' in v || 'm' in v); /** Value at a breakpoint following the desktop-first cascade. */ export function cascade(v, bp) { if (!isResp(v)) return { v, from: v !== undefined ? 'd' : null }; const idx = BP_IDS.indexOf(bp); for (let i = idx; i >= 0; i--) { const x = v[BP_IDS[i]]; if (x !== undefined && x !== '') return { v: x, from: BP_IDS[i] }; } return { v: undefined, from: null }; } /** Evaluate control conditions: { key: value | [values] | '!key' | '?key' } */ export function visible(c, s, styleOf) { if (!c.c) return true; for (const k in c.c) { const want = c.c[k]; if (k.startsWith('?')) { const v = s[k.slice(1)]; const has = Array.isArray(v) ? v.length > 0 : v && typeof v === 'object' ? Object.keys(v).length > 0 : !!v; if (has !== !!want) return false; continue; } if (k.startsWith('!')) { const v = s[k.slice(1)]; if (want === true ? !!v : v === want) return false; continue; } let v = s[k]; if (v === undefined && styleOf) v = styleOf(k); if (isResp(v)) v = v.d; if (Array.isArray(want) ? !want.includes(v) : v !== want) return false; } return true; } /** Render the controls list of a widget (content tab). */ export function ControlsList({ id, el, def }) { const device = useStore((s) => s.device); const s = { ...widgetDefaults(el.t), ...(el.s || {}) }; const styleOf = (name) => { const c = def.controls.find((x) => x.n === name); if (!c || !c.style) return undefined; return cascade(el.st?.[c.style.part || 'root']?.n, device).v?.[c.style.prop]; }; const groups = []; let cur = null; for (const c of def.controls || []) { if (c.sec) { cur = { title: c.sec, closed: c.closed, items: [] }; groups.push(cur); continue; } if (!cur) { cur = { title: 'Settings', items: [] }; groups.push(cur); } cur.items.push(c); } return groups.map((g, gi) => { const items = g.items.filter((c) => visible(c, s, styleOf)); if (!items.length) return null; return (
{items.map((c) => ( ))}
); }); } /** One control bound to an element. */ export function Ctl({ c, id, el, device }) { const labelRef = useRef(); const defs = widgetDefaults(el.t); if (c.style) { const part = c.style.part || 'root'; const byBp = el.st?.[part]?.n; const own = byBp?.[device]?.[c.style.prop]; const inh = own === undefined ? cascade(byBp && Object.fromEntries(Object.entries(byBp).map(([b, p]) => [b, p?.[c.style.prop]])), device) : null; const set = (v) => setStyle(id, part, 'n', device, c.style.prop, v, c.l); return ( set(undefined)} labelRef={labelRef} help={c.help} stack={wide(c)} > ); } const raw = el.s?.[c.n]; const hasOwn = raw !== undefined; let value, setV, respLabel, inheritedFrom = null, isSet = hasOwn; if (c.r) { const base = raw !== undefined ? raw : c.d; const casc = cascade(base, device); const own = isResp(base) ? base[device] : device === 'd' ? base : undefined; value = own; respLabel = bpLabel(device); if (own === undefined && casc.v !== undefined) inheritedFrom = bpLabel(casc.from); isSet = isResp(raw) ? raw[device] !== undefined : device === 'd' && raw !== undefined; setV = (v) => { let next = isResp(raw) ? { ...raw } : raw !== undefined ? { d: raw } : isResp(c.d) ? { ...c.d } : c.d !== undefined ? { d: c.d } : {}; if (v === '' || v === undefined) delete next[device]; else next[device] = v; setSetting(id, c.n, Object.keys(next).length ? next : undefined, c.l); }; var placeholder = casc.v !== undefined && own === undefined ? casc.v : c.ph; } else { value = raw !== undefined ? raw : defs[c.n]; setV = (v) => setSetting(id, c.n, v, c.l); } const dynVal = value && typeof value === 'object' && value.$dyn; const dyn = c.dyn && !dynVal ? setV(c.t === 'link' ? { url: '', $dyn: d.$dyn } : d)} /> : null; const reset = () => (c.r ? setV(undefined) : setSetting(id, c.n, undefined, __('Reset %s', c.l))); if (c.t === 'note') return (
{c.l}
); return ( {dyn} : null} > {dynVal ? ( ) : ( )} ); } const wide = (c) => [ 'repeater', 'rich', 'code', 'textarea', 'gallery', 'media', 'group', 'link', 'posts', 'terms', 'multi', 'icon', 'template', 'menu', ].includes(c.t); /** Bare input for a control type. Also used inside repeaters and groups. */ export function Input({ c, value, onChange, placeholder, scrubRef, id, el, dyn, path }) { switch (c.t) { case 'text': return (
{dyn}
); case 'textarea': return ; case 'number': return ( onChange(v === '' ? undefined : +v)} unitless placeholder={placeholder !== undefined ? String(placeholder) : ''} min={c.min} max={c.max} step={c.step || 1} scrubRef={scrubRef} /> ); case 'slider': return ; case 'select': { let opts = c.o === 'postTypes' ? [ ...(CFG.postTypes || [ ['post', 'Posts'], ['page', 'Pages'], ]), ...(c.extra || []), ] : c.o; opts = opts.map((o) => (Array.isArray(o) ? [o[0], tr(o[1]), ...o.slice(2)] : [o, String(o)])); return (
{dyn}
); } case 'choose': return onChange(v === value ? '' : v)} options={c.o} size="sm" />; case 'switch': return ; case 'color': return ; case 'media': return ; case 'gallery': return ; case 'icon': return ; case 'link': return ; case 'rich': return ; case 'code': return ; case 'datetime': return onChange(e.target.value)} />; case 'multi': return ; case 'posts': return ; case 'terms': return ; case 'template': return ; case 'menu': return ; case 'popup': return ; case 'repeater': return ; case 'group': return ; case 'font': return ; default: return ; } } function TextIn({ value, onChange, placeholder, multiline, rows = 3 }) { const [v, setV] = useState(value ?? ''); const focused = useRef(false); useEffect(() => { if (!focused.current) setV(value ?? ''); }, [value]); const props = { value: v, placeholder: placeholder !== undefined ? String(placeholder) : undefined, spellCheck: multiline, onFocus: () => (focused.current = true), onBlur: () => { focused.current = false; if (v !== (value ?? '')) onChange(v); }, onChange: (e) => { setV(e.target.value); onChange(e.target.value); }, }; return multiline ? (