DevTools Regex: Tastatur-Bug behoben (RuleCard als Top-Level), Zahlenbereich enthält/exakt
This commit is contained in:
@@ -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 (0–9)` };
|
||||
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}}
|
||||
|
||||
Reference in New Issue
Block a user