DevTools Regex: Tastatur-Bug behoben (RuleCard als Top-Level), Zahlenbereich enthält/exakt

This commit is contained in:
2026-06-01 13:07:04 +02:00
parent 0b31804d21
commit 244cb37470

View File

@@ -1424,7 +1424,8 @@ const RULE_TYPES = [
{ id:'alnum_n', label:'Genau N Alphanumerisch',fields:['count'], hint:'8' },
{ id:'minlength', label:'Mindestlänge', fields:['count'], hint:'5' },
{ id:'maxlength', label:'Höchstlänge', fields:['count'], hint:'20' },
{ id:'numrange', label:'Zahlenbereich', fields:['min','max'], hint:'' },
{ id:'numrange', label:'Zahlenbereich (enthält)', fields:['min','max'], hint:'' },
{ id:'numrange_exact',label:'Zahlenbereich (exakt)', fields:['min','max'], hint:'' },
{ id:'onlynums', label:'Nur Zahlen', fields:[], hint:'' },
{ id:'onlyletters', label:'Nur Buchstaben', fields:[], hint:'' },
{ id:'onlyalnum', label:'Nur Alphanumerisch', fields:[], hint:'' },
@@ -1460,7 +1461,8 @@ function ruleToRegex(rule) {
case 'alnum_n': return { pat: `[a-zA-Z0-9]{${f.count||1}}`, desc: `genau ${f.count||1} alphanumerische Zeichen` };
case 'minlength': return { pat: `.{${f.count||1},}`, desc: `mindestens ${f.count||1} Zeichen lang` };
case 'maxlength': return { pat: `.{0,${f.count||999}}`, desc: `höchstens ${f.count||999} Zeichen lang` };
case 'numrange': return { pat: buildNumRange(Number(f.min||0), Number(f.max||9)), desc: `Zahl zwischen ${f.min||0} und ${f.max||9}` };
case 'numrange': return { pat: `(?:${buildNumRange(Number(f.min||0), Number(f.max||9))})`, desc: `enthält eine Zahl zwischen ${f.min||0} und ${f.max||9}` };
case 'numrange_exact': return { pat: `^(?:${buildNumRange(Number(f.min||0), Number(f.max||9))})$`, desc: `ist exakt eine Zahl zwischen ${f.min||0} und ${f.max||9}` };
case 'onlynums': return { pat: `^\\d+$`, desc: `nur Ziffern (09)` };
case 'onlyletters': return { pat: `^[a-zA-ZäöüÄÖÜß]+$`, desc: `nur Buchstaben` };
case 'onlyalnum': return { pat: `^[a-zA-Z0-9]+$`, desc: `nur Buchstaben und Ziffern` };
@@ -1479,10 +1481,13 @@ function ruleToRegex(rule) {
// Simple numeric range pattern builder
function buildNumRange(lo, hi) {
if (isNaN(lo) || isNaN(hi)) return '\\d+';
if (lo === hi) return String(lo);
if (lo < 0 || hi < 0) return `(-?\\d+)`; // fallback for negative
return `(${lo}|${lo+1 <= hi ? `[${lo}-${hi}]` : lo})`.replace(/\((\d+)\)/, '$1');
// For proper range, just do a simple approach:
if (lo > hi) return '\\d+';
if (hi - lo > 9999) return '\\d+'; // too large, fallback
const vals = [];
for (let i = lo; i <= hi; i++) vals.push(String(i));
return vals.join('|');
}
// Combine all rules into final regex
@@ -1621,7 +1626,73 @@ function explainRegex(pattern) {
return tokens;
}
function RegexBuilder({ toast, mobile }) {
// RuleCard als Top-Level-Komponente (nicht innerhalb von RegexBuilder — vermeidet Fokus-Bug)
const FIELD_LABELS_RC = {text:'Text', count:'Anzahl', min:'Von', max:'Bis', pattern:'Muster'};
const LBL_STYLE = {color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:10, marginBottom:3};
function RuleCard({rule, idx, onUpdate, onDelete, onMove, inpStyle}) {
const rt = RULE_TYPES.find(r=>r.id===rule.type);
const preview = ruleToRegex(rule);
return (
<div style={{borderRadius:9, background:'rgba(255,255,255,0.04)',
border:'1px solid rgba(255,255,255,0.09)', padding:'10px 12px', marginBottom:8}}>
<div style={{display:'flex', gap:8, alignItems:'flex-start', flexWrap:'wrap'}}>
{/* Move buttons */}
<div style={{display:'flex', flexDirection:'column', gap:2, paddingTop:2}}>
<button onClick={()=>onMove(rule.id,-1)} style={{background:'rgba(255,255,255,0.05)',border:'none',
borderRadius:4,color:'rgba(255,255,255,0.4)',cursor:'pointer',padding:'1px 6px',fontSize:11}}></button>
<button onClick={()=>onMove(rule.id,+1)} style={{background:'rgba(255,255,255,0.05)',border:'none',
borderRadius:4,color:'rgba(255,255,255,0.4)',cursor:'pointer',padding:'1px 6px',fontSize:11}}></button>
</div>
{/* Rule number badge */}
<div style={{minWidth:22,height:22,borderRadius:11,background:'rgba(78,205,196,0.15)',
color:'#4ecdc4',fontFamily:'monospace',fontSize:10,fontWeight:700,
display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0,marginTop:2}}>
{idx+1}
</div>
{/* Type select */}
<div style={{flex:'0 0 auto'}}>
<div style={LBL_STYLE}>Regeltyp</div>
<select style={{...inpStyle, width:'auto', cursor:'pointer'}} value={rule.type}
onChange={e=>onUpdate(rule.id,{type:e.target.value, fields:{}})}>
{RULE_TYPES.map(rt=>(
<option key={rt.id} value={rt.id}>{rt.label}</option>
))}
</select>
</div>
{/* Dynamic fields */}
{rt?.fields.map(f=>(
<div key={f} style={{flex:'1 1 80px', minWidth:80}}>
<div style={LBL_STYLE}>{FIELD_LABELS_RC[f]||f}</div>
<input style={{...inpStyle, width:'100%'}}
placeholder={rt.hint||''}
value={rule.fields[f]||''}
onChange={e=>onUpdate(rule.id,{fields:{...rule.fields,[f]:e.target.value}})}/>
</div>
))}
{/* Preview */}
<div style={{flex:'1 1 100px', minWidth:100}}>
<div style={LBL_STYLE}>Regex-Teil</div>
<code style={{display:'block',background:'rgba(0,0,0,0.3)',borderRadius:6,
padding:'7px 9px',color:'#4ecdc4',fontFamily:"'Courier New',monospace",
fontSize:12,wordBreak:'break-all',minHeight:34}}>
{preview.pat || <span style={{color:'rgba(255,255,255,0.2)'}}></span>}
</code>
</div>
{/* Delete */}
<button onClick={()=>onDelete(rule.id)}
style={{background:'rgba(255,107,157,0.12)',border:'1px solid rgba(255,107,157,0.2)',
borderRadius:6,color:'#ff6b9d',cursor:'pointer',padding:'4px 10px',fontSize:13,
marginTop:18,flexShrink:0}}></button>
</div>
<div style={{marginTop:6,marginLeft:60,color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>
{preview.desc}
</div>
</div>
);
}
const [mode, setMode] = useState('build'); // 'build' | 'explain'
// ── Build mode ──
const [rules, setRules] = useState([]);
@@ -1677,83 +1748,17 @@ function RegexBuilder({ toast, mobile }) {
}
const inp = {...S.inp, fontSize:13, padding:'7px 9px', boxSizing:'border-box'};
const lbl = {color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:10, marginBottom:3};
const FIELD_LABELS = {text:'Text', count:'Anzahl', min:'Von', max:'Bis', pattern:'Muster'};
function RuleCard({rule, idx}) {
const rt = RULE_TYPES.find(r=>r.id===rule.type);
return (
<div style={{borderRadius:9, background:'rgba(255,255,255,0.04)',
border:'1px solid rgba(255,255,255,0.09)', padding:'10px 12px', marginBottom:8}}>
<div style={{display:'flex', gap:8, alignItems:'flex-start', flexWrap:'wrap'}}>
{/* Move buttons */}
<div style={{display:'flex', flexDirection:'column', gap:2, paddingTop:2}}>
<button onClick={()=>moveRule(rule.id,-1)} style={{background:'rgba(255,255,255,0.05)',border:'none',
borderRadius:4,color:'rgba(255,255,255,0.4)',cursor:'pointer',padding:'1px 6px',fontSize:11}}></button>
<button onClick={()=>moveRule(rule.id,+1)} style={{background:'rgba(255,255,255,0.05)',border:'none',
borderRadius:4,color:'rgba(255,255,255,0.4)',cursor:'pointer',padding:'1px 6px',fontSize:11}}></button>
</div>
{/* Rule number badge */}
<div style={{minWidth:22,height:22,borderRadius:11,background:'rgba(78,205,196,0.15)',
color:'#4ecdc4',fontFamily:'monospace',fontSize:10,fontWeight:700,
display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0,marginTop:2}}>
{idx+1}
</div>
{/* Type select */}
<div style={{flex:'0 0 auto'}}>
<div style={lbl}>Regeltyp</div>
<select style={{...inp, width:'auto', cursor:'pointer'}} value={rule.type}
onChange={e=>updateRule(rule.id,{type:e.target.value, fields:{}})}>
{RULE_TYPES.map(rt=>(
<option key={rt.id} value={rt.id}>{rt.label}</option>
))}
</select>
</div>
{/* Dynamic fields */}
{rt?.fields.map(f=>(
<div key={f} style={{flex:'1 1 80px', minWidth:80}}>
<div style={lbl}>{FIELD_LABELS[f]||f}</div>
<input style={{...inp, width:'100%'}} type={f==='pattern'?'text':'text'}
placeholder={rt.hint||''}
value={rule.fields[f]||''}
onChange={e=>updateRule(rule.id,{fields:{...rule.fields,[f]:e.target.value}})}/>
</div>
))}
{/* Preview of this rule's regex */}
<div style={{flex:'1 1 100px', minWidth:100}}>
<div style={lbl}>Regex-Teil</div>
<code style={{display:'block',background:'rgba(0,0,0,0.3)',borderRadius:6,
padding:'7px 9px',color:'#4ecdc4',fontFamily:"'Courier New',monospace",
fontSize:12,wordBreak:'break-all',minHeight:34}}>
{ruleToRegex(rule).pat || <span style={{color:'rgba(255,255,255,0.2)'}}></span>}
</code>
</div>
{/* Delete */}
<button onClick={()=>deleteRule(rule.id)}
style={{background:'rgba(255,107,157,0.12)',border:'1px solid rgba(255,107,157,0.2)',
borderRadius:6,color:'#ff6b9d',cursor:'pointer',padding:'4px 10px',fontSize:13,
marginTop:18,flexShrink:0}}></button>
</div>
{/* Description of this rule */}
<div style={{marginTop:6,marginLeft:60,color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>
{ruleToRegex(rule).desc}
</div>
</div>
);
}
return (
<div>
{/* Mode tabs */}
<div style={{display:'flex',gap:6,marginBottom:20}}>
{[['build','🔨 Regex bauen'],['explain','🔍 Regex erklären']].map(([id,lbl])=>(
{[['build','🔨 Regex bauen'],['explain','🔍 Regex erklären']].map(([id,tabLbl])=>(
<button key={id} onClick={()=>setMode(id)} style={{
padding:'7px 18px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
background:mode===id?'#4ecdc4':'rgba(255,255,255,0.05)',
color:mode===id?'#0d0d0f':'rgba(255,255,255,0.65)',
border:mode===id?'none':'1px solid rgba(255,255,255,0.12)',
fontWeight:mode===id?700:400}}>{lbl}</button>
fontWeight:mode===id?700:400}}>{tabLbl}</button>
))}
</div>
@@ -1761,7 +1766,8 @@ function RegexBuilder({ toast, mobile }) {
<div>
{/* Regel-Karten */}
{rules.map((rule,idx)=>(
<RuleCard key={rule.id} rule={rule} idx={idx}/>
<RuleCard key={rule.id} rule={rule} idx={idx}
onUpdate={updateRule} onDelete={deleteRule} onMove={moveRule} inpStyle={inp}/>
))}
<button style={{...S.btn('#4ecdc4',false), fontSize:12, padding:'8px 18px', marginBottom:20}}