UI: Ein-Zeichen-Inputs mit +, Klick-Entfernen, alphabetische Sortierung und Drag&Drop auf pos1–pos5; Button/Reset-Styles vereinheitlicht; Abstände/Alignment angepasst; README aktualisiert

This commit is contained in:
2025-08-20 09:45:02 +02:00
parent d730e6b266
commit a52a7ac2cf
2 changed files with 150 additions and 8 deletions

View File

@@ -5,6 +5,10 @@ HilfsWebApp für deutsche WordleRätsel. Nutzer geben bekannte Buchstab
## Features ## Features
- Filter nach Positionen (15), enthaltenen und ausgeschlossenen Buchstaben - Filter nach Positionen (15), enthaltenen und ausgeschlossenen Buchstaben
- Enthaltene/Ausgeschlossene Buchstaben per EinZeichenEingabe und „+“-Button hinzufügen
- Ausgewählte Buchstaben werden als Badges angezeigt, Klick entfernt den Buchstaben wieder
- Alphabetische Sortierung der ausgewählten Buchstaben (deutsche Locale)
- DragandDrop: Buchstaben aus „Enthalten“ direkt auf die Felder `pos1``pos5` ziehen
- Deutsche Wortliste (nur 5 Buchstaben), aus OpenThesaurus und wordfreq gemerged - Deutsche Wortliste (nur 5 Buchstaben), aus OpenThesaurus und wordfreq gemerged
- QuellenBadges je Treffer (OT/WF) - QuellenBadges je Treffer (OT/WF)
- Zugängliche UI (A11y: Fieldset/Legend, ARIAHinweise, SkipLink, semantische Liste) - Zugängliche UI (A11y: Fieldset/Legend, ARIAHinweise, SkipLink, semantische Liste)

View File

@@ -40,15 +40,18 @@
.grid { display: grid; grid-template-columns: repeat(5, 3rem); gap: .5rem; } .grid { display: grid; grid-template-columns: repeat(5, 3rem); gap: .5rem; }
.grid input { text-align: center; font-size: 1.25rem; padding: .4rem; background: var(--input-bg); color: var(--input-text); border: 1px solid var(--border); border-radius: .375rem; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; text-transform: uppercase; } .grid input { text-align: center; font-size: 1.25rem; padding: .4rem; background: var(--input-bg); color: var(--input-text); border: 1px solid var(--border); border-radius: .375rem; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; text-transform: uppercase; }
.text-input { font-size: 1.1rem; padding: .5rem; background: var(--input-bg); color: var(--input-text); border: 1px solid var(--border); border-radius: .375rem; width: 100%; box-sizing: border-box; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; text-transform: uppercase; } .text-input { font-size: 1.1rem; padding: .5rem; background: var(--input-bg); color: var(--input-text); border: 1px solid var(--border); border-radius: .375rem; width: 100%; box-sizing: border-box; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; text-transform: uppercase; }
.letter-input { width: calc(1ch + 2rem); padding: .5rem 1rem; font-size: 1rem; line-height: 1; background: var(--input-bg); color: var(--input-text); border: 1px solid var(--border); border-radius: .5rem; text-align: center; text-transform: uppercase; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; }
.plus-button { width: calc(1ch + 2rem); text-align: center; line-height: 1; display: inline-flex; align-items: center; justify-content: center; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; }
label { font-weight: 600; display: block; margin-top: 1rem; margin-bottom: .25rem; } label { font-weight: 600; display: block; margin-top: 1rem; margin-bottom: .25rem; }
.results { margin-top: 1.5rem; } .results { margin-top: 1.5rem; }
.results-box { border: 1px solid var(--border); border-radius: .5rem; padding: .75rem; } .results-box { border: 1px solid var(--border); border-radius: .5rem; padding: .75rem; }
.badge { display: inline-block; padding: .25rem .5rem; background: var(--badge-bg); color: var(--badge-text); border-radius: .375rem; margin-right: .25rem; margin-bottom: .25rem; } .badge { display: inline-block; padding: .25rem .5rem; background: var(--badge-bg); color: var(--badge-text); border-radius: .375rem; margin-right: .25rem; margin-bottom: .25rem; }
#includes-list .badge, #excludes-list .badge { cursor: pointer; }
.source { font-size: .75rem; padding: .1rem .35rem; border-radius: .25rem; margin-left: .25rem; } .source { font-size: .75rem; padding: .1rem .35rem; border-radius: .25rem; margin-left: .25rem; }
.source.ot { background: #dbeafe; color: #1e40af; } .source.ot { background: #dbeafe; color: #1e40af; }
.source.wf { background: #dcfce7; color: #065f46; } .source.wf { background: #dcfce7; color: #065f46; }
button { margin-top: 1rem; padding: .5rem 1rem; font-size: 1rem; margin-right: 0.5rem; } button { margin-top: 1rem; padding: .5rem 1rem; font-size: 1rem; margin-right: 0.5rem; background: var(--button-bg); color: var(--button-text); border: 1px solid var(--border); border-radius: .5rem; cursor: pointer; }
.reset-button { background: var(--muted); color: var(--bg); } .reset-button { background: var(--muted); }
summary { cursor: pointer; } summary { cursor: pointer; }
.footer { margin-top: 2rem; font-size: .9rem; color: var(--muted); } .footer { margin-top: 2rem; font-size: .9rem; color: var(--muted); }
.footer a { color: inherit; text-decoration: underline; } .footer a { color: inherit; text-decoration: underline; }
@@ -59,11 +62,14 @@
.inline-controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; } .inline-controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.filter-box { border: 1px solid var(--border); border-radius: .5rem; padding: .5rem .75rem; margin: .5rem 0 1rem; } .filter-box { border: 1px solid var(--border); border-radius: .5rem; padding: .5rem .75rem; margin: .5rem 0 1rem; }
.word-list { list-style: none; padding: 0; margin: 0; } .word-list { list-style: none; padding: 0; margin: 0; }
#includes-list, #excludes-list { margin-top: .75rem; }
.word-list li { display: inline-block; margin: 0 .25rem .25rem 0; } .word-list li { display: inline-block; margin: 0 .25rem .25rem 0; }
fieldset { border: 1px solid var(--border); border-radius: .5rem; padding: .75rem; } fieldset { border: 1px solid var(--border); border-radius: .5rem; padding: .75rem; }
legend { font-weight: 700; padding: 0 .25rem; } legend { font-weight: 700; padding: 0 .25rem; }
.theme-toggle { background: var(--button-bg); color: var(--button-text); border: 1px solid var(--border); border-radius: .5rem; padding: .4rem .6rem; font-size: .95rem; margin-top: 0; } .theme-toggle { background: var(--button-bg); color: var(--button-text); border: 1px solid var(--border); border-radius: .5rem; padding: .4rem .6rem; font-size: .95rem; margin-top: 0; }
.theme-toggle:focus { outline: 2px solid #3b82f6; outline-offset: 2px; } .theme-toggle:focus { outline: 2px solid #3b82f6; outline-offset: 2px; }
.inline-controls .plus-button { margin-top: 0; margin-right: 0; }
.drop-target { outline: 2px dashed #3b82f6; outline-offset: 2px; }
</style> </style>
</head> </head>
<body> <body>
@@ -91,13 +97,23 @@
<p id="pos-hint" class="hint">Je Feld genau ein Buchstabe. Umlaute (ä, ö, ü) und ß sind erlaubt.</p> <p id="pos-hint" class="hint">Je Feld genau ein Buchstabe. Umlaute (ä, ö, ü) und ß sind erlaubt.</p>
</fieldset> </fieldset>
<label for="includes">Weitere enthaltene Buchstaben (beliebige Reihenfolge)</label> <label for="includes-input-one">Weitere enthaltene Buchstaben (beliebige Reihenfolge)</label>
<input id="includes" name="includes" class="text-input" aria-describedby="includes-hint" inputmode="text" autocomplete="off" value="{{ includes }}" /> <div class="inline-controls" aria-describedby="includes-hint">
<p id="includes-hint" class="hint">Mehrere Buchstaben ohne Trennzeichen eingeben (z.B. „aei“).</p> <input id="includes-input-one" maxlength="1" aria-label="Buchstabe hinzufügen (enthalten)" inputmode="text" autocomplete="off" pattern="[A-Za-zÄÖÜäöüß]" class="letter-input" />
<button type="button" id="includes-add-button" class="plus-button" aria-label="Buchstabe zu 'Enthalten' hinzufügen">+</button>
<input type="hidden" id="includes" name="includes" value="{{ includes }}" />
</div>
<ul id="includes-list" class="word-list" aria-live="polite"></ul>
<p id="includes-hint" class="hint">Gib einen Buchstaben ein und füge ihn mit „+“ zur Liste hinzu.</p>
<label for="excludes">Ausgeschlossene Buchstaben</label> <label for="excludes-input-one">Ausgeschlossene Buchstaben</label>
<input id="excludes" name="excludes" class="text-input" aria-describedby="excludes-hint" inputmode="text" autocomplete="off" value="{{ excludes }}" /> <div class="inline-controls" aria-describedby="excludes-hint">
<p id="excludes-hint" class="hint">Buchstaben, die nicht vorkommen (z.B. „rst“).</p> <input id="excludes-input-one" maxlength="1" aria-label="Buchstabe hinzufügen (ausschließen)" inputmode="text" autocomplete="off" pattern="[A-Za-zÄÖÜäöüß]" class="letter-input" />
<button type="button" id="excludes-add-button" class="plus-button" aria-label="Buchstabe zu 'Ausschließen' hinzufügen">+</button>
<input type="hidden" id="excludes" name="excludes" value="{{ excludes }}" />
</div>
<ul id="excludes-list" class="word-list" aria-live="polite"></ul>
<p id="excludes-hint" class="hint">Gib einen Buchstaben ein und füge ihn mit „+“ zur Liste hinzu.</p>
<button type="submit">Suchen</button> <button type="submit">Suchen</button>
<button type="button" id="reset-button" class="reset-button">Zurücksetzen</button> <button type="button" id="reset-button" class="reset-button">Zurücksetzen</button>
@@ -201,6 +217,64 @@
var countEl = document.getElementById('visible-count'); var countEl = document.getElementById('visible-count');
if (countEl) countEl.textContent = String(visible); if (countEl) countEl.textContent = String(visible);
} }
function updateLettersList(listElementId, lettersString) {
var ul = document.getElementById(listElementId);
if (!ul) return;
while (ul.firstChild) ul.removeChild(ul.firstChild);
var letters = (lettersString || '')
.split('')
.filter(function(ch){ return !!ch; })
.map(function(ch){ return ch.toLowerCase(); })
.sort(function(a,b){ return a.localeCompare(b, 'de'); });
letters.forEach(function(ch){
var li = document.createElement('li');
var badge = document.createElement('span');
badge.className = 'badge';
var lower = ch.toLowerCase();
badge.textContent = lower.toUpperCase();
badge.setAttribute('data-letter', lower);
badge.title = 'Zum Entfernen klicken';
badge.setAttribute('aria-label', "Buchstabe '" + lower.toUpperCase() + "' entfernen");
// Drag & Drop für Includes-Liste aktivieren
if (listElementId === 'includes-list') {
badge.draggable = true;
badge.addEventListener('dragstart', function(e){
try {
e.dataTransfer.setData('text/plain', lower);
e.dataTransfer.setData('text/list', listElementId);
e.dataTransfer.effectAllowed = 'move';
} catch (err) {}
});
}
li.appendChild(badge);
ul.appendChild(li);
});
}
function removeLetterFromList(hiddenId, listId, letter) {
var hidden = document.getElementById(hiddenId);
if (!hidden) return;
var current = hidden.value || '';
var next = (current || '').split('').filter(function(ch){ return ch !== letter; }).join('');
hidden.value = next;
updateLettersList(listId, next);
}
function addLetterFromInput(inputId, hiddenId, listId) {
var input = document.getElementById(inputId);
var hidden = document.getElementById(hiddenId);
if (!input || !hidden) return;
var raw = (input.value || '').trim();
if (!raw) return;
var ch = raw[0];
if (!/[A-Za-zÄÖÜäöüß]/.test(ch)) { input.value = ''; return; }
var lower = ch.toLowerCase();
var current = hidden.value || '';
if (current.indexOf(lower) === -1) {
hidden.value = current + lower;
updateLettersList(listId, hidden.value);
}
input.value = '';
input.focus();
}
window.addEventListener('load', function() { window.addEventListener('load', function() {
var ot = document.getElementById('filter-ot'); var ot = document.getElementById('filter-ot');
var wf = document.getElementById('filter-wf'); var wf = document.getElementById('filter-wf');
@@ -209,6 +283,62 @@
if (wf) wf.addEventListener('change', applyFilters); if (wf) wf.addEventListener('change', applyFilters);
if (uml) uml.addEventListener('change', applyFilters); if (uml) uml.addEventListener('change', applyFilters);
applyFilters(); applyFilters();
// Listen initial aus Hidden-Feldern rendern
var hiddenInc = document.getElementById('includes');
var hiddenExc = document.getElementById('excludes');
updateLettersList('includes-list', hiddenInc ? hiddenInc.value : '');
updateLettersList('excludes-list', hiddenExc ? hiddenExc.value : '');
// Add-Buttons verdrahten
var addIncBtn = document.getElementById('includes-add-button');
if (addIncBtn) addIncBtn.addEventListener('click', function(){ addLetterFromInput('includes-input-one', 'includes', 'includes-list'); });
var addExcBtn = document.getElementById('excludes-add-button');
if (addExcBtn) addExcBtn.addEventListener('click', function(){ addLetterFromInput('excludes-input-one', 'excludes', 'excludes-list'); });
// Enter-Handling in den Ein-Feld-Eingaben
var incInputOne = document.getElementById('includes-input-one');
if (incInputOne) incInputOne.addEventListener('keydown', function(e){ if (e.key === 'Enter') { e.preventDefault(); addLetterFromInput('includes-input-one', 'includes', 'includes-list'); } });
var excInputOne = document.getElementById('excludes-input-one');
if (excInputOne) excInputOne.addEventListener('keydown', function(e){ if (e.key === 'Enter') { e.preventDefault(); addLetterFromInput('excludes-input-one', 'excludes', 'excludes-list'); } });
// Entfernen per Klick auf Badge (Event Delegation)
var incList = document.getElementById('includes-list');
if (incList) incList.addEventListener('click', function(e){
var t = e.target;
if (t && t.classList && t.classList.contains('badge')) {
var letter = (t.getAttribute('data-letter') || t.textContent || '').trim().toLowerCase();
if (letter) removeLetterFromList('includes', 'includes-list', letter);
}
});
var excList = document.getElementById('excludes-list');
if (excList) excList.addEventListener('click', function(e){
var t = e.target;
if (t && t.classList && t.classList.contains('badge')) {
var letter = (t.getAttribute('data-letter') || t.textContent || '').trim().toLowerCase();
if (letter) removeLetterFromList('excludes', 'excludes-list', letter);
}
});
// Drop-Ziele für Positionsfelder (pos1..pos5)
for (var i = 1; i <= 5; i++) {
(function(idx){
var input = document.getElementById('pos' + idx);
if (!input) return;
input.addEventListener('dragover', function(e){
e.preventDefault();
try { e.dataTransfer.dropEffect = 'move'; } catch (err) {}
input.classList.add('drop-target');
});
input.addEventListener('dragleave', function(){ input.classList.remove('drop-target'); });
input.addEventListener('drop', function(e){
e.preventDefault();
input.classList.remove('drop-target');
var letter = (e.dataTransfer.getData('text/plain') || '').trim().toLowerCase();
var sourceList = e.dataTransfer.getData('text/list') || '';
if (letter && sourceList === 'includes-list' && /[a-zäöüß]/i.test(letter)) {
input.value = letter;
removeLetterFromList('includes', 'includes-list', letter);
input.focus();
}
});
})(i);
}
// Reset-Button Funktionalität // Reset-Button Funktionalität
var resetButton = document.getElementById('reset-button'); var resetButton = document.getElementById('reset-button');
@@ -223,9 +353,17 @@
// Weitere Felder zurücksetzen // Weitere Felder zurücksetzen
var includesField = document.getElementById('includes'); var includesField = document.getElementById('includes');
if (includesField) includesField.value = ''; if (includesField) includesField.value = '';
var includesList = document.getElementById('includes-list');
if (includesList) includesList.innerHTML = '';
var incInput = document.getElementById('includes-input-one');
if (incInput) incInput.value = '';
var excludesField = document.getElementById('excludes'); var excludesField = document.getElementById('excludes');
if (excludesField) excludesField.value = ''; if (excludesField) excludesField.value = '';
var excludesList = document.getElementById('excludes-list');
if (excludesList) excludesList.innerHTML = '';
var excInput = document.getElementById('excludes-input-one');
if (excInput) excInput.value = '';
// Suchergebnisse ausblenden // Suchergebnisse ausblenden
var resultsDiv = document.getElementById('results'); var resultsDiv = document.getElementById('results');