feat: VideoKonverter v4.0.2 - FocusManager-Fix, Poster-Caching, Performance
- FocusManager: Navigation von Nav-Leiste direkt zu Content-Karten - Input/Select Editier-Modus: Erst Enter zum Bearbeiten, D-Pad navigiert weiter - Poster lokal cachen + Pillow-Resize (233KB → 47KB, 80% kleiner) - Content-Visibility fuer versteckte View-Container Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c7151e8bd1
commit
e8f2d49949
5 changed files with 235 additions and 21 deletions
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -2,6 +2,30 @@
|
||||||
|
|
||||||
Alle relevanten Aenderungen am VideoKonverter-Projekt.
|
Alle relevanten Aenderungen am VideoKonverter-Projekt.
|
||||||
|
|
||||||
|
## [4.0.2] - 2026-03-01
|
||||||
|
|
||||||
|
### TV-App: FocusManager-Fix, Poster-Caching, Performance
|
||||||
|
|
||||||
|
#### Bugfixes
|
||||||
|
- **FocusManager Navigation**: Von der oberen Nav-Leiste nach unten navigieren springt jetzt direkt zu Content-Karten (Grid/Liste/Detail) statt bei Filter-Dropdowns und View-Switch-Buttons haengenzubleiben
|
||||||
|
- **Input/Select Editier-Modus**: Textfelder und Select-Dropdowns werden erst nach Enter-Bestaetigung editierbar - D-Pad Navigation kann jetzt ueber Formularfelder hinweg navigieren ohne haengenzubleiben
|
||||||
|
- **Content-Focus-Speicher**: `_lastContentFocus` merkt sich nur noch echte Content-Elemente (Karten, Listen-Eintraege), nicht mehr Filter/Controls
|
||||||
|
|
||||||
|
#### Performance
|
||||||
|
- **Poster-Caching mit Resize**: Poster-Bilder werden beim ersten Abruf lokal in `.metadata/` gespeichert und auf 300px Breite verkleinert (Pillow)
|
||||||
|
- 80% kleinere Bilder (233KB → 47KB pro Poster)
|
||||||
|
- Kein externer TVDB-Request mehr nach erstem Laden
|
||||||
|
- Cache-Hit: ~10ms statt ~80ms
|
||||||
|
- **Content-Visibility**: Versteckte View-Container nutzen `content-visibility: hidden` fuer bessere Render-Performance
|
||||||
|
|
||||||
|
#### Geaenderte Dateien (4 Dateien, +211/-21 Zeilen)
|
||||||
|
- `app/routes/library_api.py` - On-Demand Poster-Download + Pillow-Resize-Cache
|
||||||
|
- `app/routes/tv_api.py` - `_localize_posters()` Helper fuer lokale Poster-URLs
|
||||||
|
- `app/static/tv/css/tv.css` - Input-Editing-Style, Content-Visibility
|
||||||
|
- `app/static/tv/js/tv.js` - FocusManager: Input-Modus, Nav→Content Fix, initFocus Fix
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [4.0.1] - 2026-03-01
|
## [4.0.1] - 2026-03-01
|
||||||
|
|
||||||
### TV-App: UX-Verbesserungen & Bugfixes
|
### TV-App: UX-Verbesserungen & Bugfixes
|
||||||
|
|
|
||||||
|
|
@ -323,22 +323,93 @@ def setup_library_routes(app: web.Application, config: Config,
|
||||||
return web.json_response(results)
|
return web.json_response(results)
|
||||||
|
|
||||||
async def get_metadata_image(request: web.Request) -> web.Response:
|
async def get_metadata_image(request: web.Request) -> web.Response:
|
||||||
"""GET /api/library/metadata/{series_id}/{filename}"""
|
"""GET /api/library/metadata/{series_id}/{filename}?w=300
|
||||||
|
Laedt Bilder lokal aus .metadata/ oder downloaded on-demand von TVDB.
|
||||||
|
Optionaler Parameter w= verkleinert auf angegebene Breite (gecacht)."""
|
||||||
|
import os
|
||||||
|
import aiohttp as aiohttp_client
|
||||||
|
|
||||||
series_id = int(request.match_info["series_id"])
|
series_id = int(request.match_info["series_id"])
|
||||||
filename = request.match_info["filename"]
|
filename = request.match_info["filename"]
|
||||||
detail = await library_service.get_series_detail(series_id)
|
detail = await library_service.get_series_detail(series_id)
|
||||||
if not detail or not detail.get("folder_path"):
|
if not detail:
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"error": "Nicht gefunden"}, status=404
|
{"error": "Nicht gefunden"}, status=404
|
||||||
)
|
)
|
||||||
import os
|
|
||||||
file_path = os.path.join(
|
folder_path = detail.get("folder_path", "")
|
||||||
detail["folder_path"], ".metadata", filename
|
meta_dir = os.path.join(folder_path, ".metadata") if folder_path else ""
|
||||||
)
|
file_path = os.path.join(meta_dir, filename) if meta_dir else ""
|
||||||
if not os.path.isfile(file_path):
|
|
||||||
|
# Lokale Datei nicht vorhanden? On-demand von TVDB downloaden
|
||||||
|
if not file_path or not os.path.isfile(file_path):
|
||||||
|
poster_url = detail.get("poster_url", "")
|
||||||
|
if filename.startswith("poster") and poster_url and folder_path:
|
||||||
|
os.makedirs(meta_dir, exist_ok=True)
|
||||||
|
try:
|
||||||
|
async with aiohttp_client.ClientSession() as session:
|
||||||
|
async with session.get(
|
||||||
|
poster_url,
|
||||||
|
timeout=aiohttp_client.ClientTimeout(total=15)
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.read()
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
logging.info(
|
||||||
|
f"Poster heruntergeladen: Serie {series_id}"
|
||||||
|
f" ({len(data)} Bytes)")
|
||||||
|
else:
|
||||||
|
# Download fehlgeschlagen -> Redirect
|
||||||
|
raise web.HTTPFound(poster_url)
|
||||||
|
except web.HTTPFound:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(
|
||||||
|
f"Poster-Download fehlgeschlagen fuer Serie "
|
||||||
|
f"{series_id}: {e}")
|
||||||
|
if poster_url:
|
||||||
|
raise web.HTTPFound(poster_url)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"error": "Datei nicht gefunden"}, status=404
|
{"error": "Datei nicht gefunden"}, status=404
|
||||||
)
|
)
|
||||||
|
elif filename.startswith("poster") and poster_url:
|
||||||
|
# Kein Ordner -> Redirect zur externen URL
|
||||||
|
raise web.HTTPFound(poster_url)
|
||||||
|
else:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Datei nicht gefunden"}, status=404
|
||||||
|
)
|
||||||
|
|
||||||
|
# Resize-Parameter: ?w=300 verkleinert auf max. 300px Breite
|
||||||
|
width_param = request.query.get("w")
|
||||||
|
if width_param:
|
||||||
|
try:
|
||||||
|
target_w = int(width_param)
|
||||||
|
if 50 <= target_w <= 1000:
|
||||||
|
base, _ = os.path.splitext(filename)
|
||||||
|
cache_name = f"{base}_w{target_w}.jpg"
|
||||||
|
cache_path = os.path.join(meta_dir, cache_name)
|
||||||
|
if not os.path.isfile(cache_path):
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
with Image.open(file_path) as img:
|
||||||
|
if img.width > target_w:
|
||||||
|
ratio = target_w / img.width
|
||||||
|
new_h = int(img.height * ratio)
|
||||||
|
img = img.resize(
|
||||||
|
(target_w, new_h), Image.LANCZOS
|
||||||
|
)
|
||||||
|
img = img.convert("RGB")
|
||||||
|
img.save(cache_path, "JPEG", quality=80)
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(
|
||||||
|
f"Poster-Resize fehlgeschlagen: {e}")
|
||||||
|
return web.FileResponse(file_path)
|
||||||
|
return web.FileResponse(cache_path)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
return web.FileResponse(file_path)
|
return web.FileResponse(file_path)
|
||||||
|
|
||||||
# === Filme ===
|
# === Filme ===
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,22 @@ def setup_tv_routes(app: web.Application, config: Config,
|
||||||
library_service: LibraryService) -> None:
|
library_service: LibraryService) -> None:
|
||||||
"""Registriert alle TV-App Routes"""
|
"""Registriert alle TV-App Routes"""
|
||||||
|
|
||||||
|
# --- Poster-URL Lokalisierung ---
|
||||||
|
# TVDB-URLs durch lokalen Endpunkt ersetzen (schnelleres Laden)
|
||||||
|
|
||||||
|
_POSTER_WIDTH = 300 # Max. Breite fuer Poster-Thumbnails
|
||||||
|
|
||||||
|
def _localize_posters(rows, content_type="series"):
|
||||||
|
"""poster_url durch lokalen Resize-Endpunkt ersetzen.
|
||||||
|
Vermeidet externe TVDB-Requests, Bilder werden lokal gecacht."""
|
||||||
|
for row in rows:
|
||||||
|
if content_type == "series":
|
||||||
|
row["poster_url"] = (
|
||||||
|
f"/api/library/metadata/{row['id']}"
|
||||||
|
f"/poster.jpg?w={_POSTER_WIDTH}"
|
||||||
|
)
|
||||||
|
# Filme: noch keine lokale Metadaten -> URL beibehalten
|
||||||
|
|
||||||
# --- Auth-Hilfsfunktionen ---
|
# --- Auth-Hilfsfunktionen ---
|
||||||
|
|
||||||
async def get_tv_user(request: web.Request) -> dict | None:
|
async def get_tv_user(request: web.Request) -> dict | None:
|
||||||
|
|
@ -193,6 +209,9 @@ def setup_tv_routes(app: web.Application, config: Config,
|
||||||
await cur.execute(movies_query, params)
|
await cur.execute(movies_query, params)
|
||||||
movies = await cur.fetchall()
|
movies = await cur.fetchall()
|
||||||
|
|
||||||
|
# Poster-URLs lokalisieren (kein TVDB-Laden)
|
||||||
|
_localize_posters(series, "series")
|
||||||
|
|
||||||
# Weiterschauen
|
# Weiterschauen
|
||||||
continue_watching = await auth_service.get_continue_watching(
|
continue_watching = await auth_service.get_continue_watching(
|
||||||
user["id"]
|
user["id"]
|
||||||
|
|
@ -304,6 +323,9 @@ def setup_tv_routes(app: web.Application, config: Config,
|
||||||
if g:
|
if g:
|
||||||
all_genres.add(g)
|
all_genres.add(g)
|
||||||
|
|
||||||
|
# Poster-URLs lokalisieren (kein TVDB-Laden)
|
||||||
|
_localize_posters(series, "series")
|
||||||
|
|
||||||
# Ordner-Daten aufbereiten (gruppiert nach Quelle, sortiert nach Ordnername)
|
# Ordner-Daten aufbereiten (gruppiert nach Quelle, sortiert nach Ordnername)
|
||||||
folder_data = []
|
folder_data = []
|
||||||
src_map = {str(src["id"]): src["name"] for src in sources}
|
src_map = {str(src["id"]): src["name"] for src in sources}
|
||||||
|
|
@ -441,6 +463,9 @@ def setup_tv_routes(app: web.Application, config: Config,
|
||||||
if not series:
|
if not series:
|
||||||
raise web.HTTPFound("/tv/series")
|
raise web.HTTPFound("/tv/series")
|
||||||
|
|
||||||
|
# Poster-URL lokalisieren
|
||||||
|
_localize_posters([series], "series")
|
||||||
|
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
"tv/series_detail.html", request, {
|
"tv/series_detail.html", request, {
|
||||||
"user": user,
|
"user": user,
|
||||||
|
|
@ -775,6 +800,9 @@ def setup_tv_routes(app: web.Application, config: Config,
|
||||||
""", (search_term, search_term))
|
""", (search_term, search_term))
|
||||||
results_movies = await cur.fetchall()
|
results_movies = await cur.fetchall()
|
||||||
|
|
||||||
|
# Poster-URLs lokalisieren
|
||||||
|
_localize_posters(results_series, "series")
|
||||||
|
|
||||||
# Such-History speichern
|
# Such-History speichern
|
||||||
await auth_service.save_search(user["id"], query)
|
await auth_service.save_search(user["id"], query)
|
||||||
else:
|
else:
|
||||||
|
|
@ -1066,6 +1094,8 @@ def setup_tv_routes(app: web.Application, config: Config,
|
||||||
"""GET /tv/watchlist - Merkliste anzeigen"""
|
"""GET /tv/watchlist - Merkliste anzeigen"""
|
||||||
user = request["tv_user"]
|
user = request["tv_user"]
|
||||||
wl = await auth_service.get_watchlist(user["id"])
|
wl = await auth_service.get_watchlist(user["id"])
|
||||||
|
# Poster-URLs lokalisieren
|
||||||
|
_localize_posters(wl["series"], "series")
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
"tv/watchlist.html", request, {
|
"tv/watchlist.html", request, {
|
||||||
"user": user,
|
"user": user,
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,17 @@ a { color: var(--accent); text-decoration: none; }
|
||||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
/* Versteckte Ansichten vom Rendering ausschliessen */
|
||||||
|
[style*="display:none"].tv-view-grid,
|
||||||
|
[style*="display:none"].tv-view-list,
|
||||||
|
[style*="display:none"].tv-view-detail,
|
||||||
|
[style*="display:none"].tv-view-folder,
|
||||||
|
[style*="display: none"].tv-view-grid,
|
||||||
|
[style*="display: none"].tv-view-list,
|
||||||
|
[style*="display: none"].tv-view-detail,
|
||||||
|
[style*="display: none"].tv-view-folder {
|
||||||
|
content-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* === Poster-Karten === */
|
/* === Poster-Karten === */
|
||||||
.tv-card {
|
.tv-card {
|
||||||
|
|
@ -1307,6 +1318,13 @@ select.select-editing {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 0 0 2px var(--accent);
|
box-shadow: 0 0 0 2px var(--accent);
|
||||||
}
|
}
|
||||||
|
/* INPUT/TEXTAREA im Editier-Modus */
|
||||||
|
input.input-editing,
|
||||||
|
textarea.input-editing {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--accent);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
.color-picker-grid {
|
.color-picker-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ class FocusManager {
|
||||||
this._lastContentFocus = null;
|
this._lastContentFocus = null;
|
||||||
// SELECT-Editier-Modus: erst Enter druecken, dann Hoch/Runter aendert Werte
|
// SELECT-Editier-Modus: erst Enter druecken, dann Hoch/Runter aendert Werte
|
||||||
this._selectActive = false;
|
this._selectActive = false;
|
||||||
|
// INPUT/TEXTAREA Editier-Modus: erst Enter druecken, dann tippen
|
||||||
|
this._inputActive = false;
|
||||||
|
|
||||||
// Tastatur-Events abfangen
|
// Tastatur-Events abfangen
|
||||||
document.addEventListener("keydown", (e) => this._onKeyDown(e));
|
document.addEventListener("keydown", (e) => this._onKeyDown(e));
|
||||||
|
|
@ -26,11 +28,21 @@ class FocusManager {
|
||||||
document.querySelectorAll(".select-editing").forEach(
|
document.querySelectorAll(".select-editing").forEach(
|
||||||
el => el.classList.remove("select-editing"));
|
el => el.classList.remove("select-editing"));
|
||||||
}
|
}
|
||||||
|
// INPUT-Editier-Modus beenden wenn Focus sich aendert
|
||||||
|
if (this._inputActive && e.target &&
|
||||||
|
e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
|
||||||
|
this._inputActive = false;
|
||||||
|
document.querySelectorAll(".input-editing").forEach(
|
||||||
|
el => el.classList.remove("input-editing"));
|
||||||
|
}
|
||||||
if (e.target && e.target.hasAttribute && e.target.hasAttribute("data-focusable")) {
|
if (e.target && e.target.hasAttribute && e.target.hasAttribute("data-focusable")) {
|
||||||
if (!e.target.closest("#tv-nav")) {
|
if (!e.target.closest("#tv-nav")) {
|
||||||
|
// Nur echte Content-Elemente merken (nicht Filter/Controls)
|
||||||
|
if (e.target.closest(".tv-grid, .tv-list-compact, .tv-detail-list, .tv-folder-view, .tv-row, .tv-episode-list")) {
|
||||||
this._lastContentFocus = e.target;
|
this._lastContentFocus = e.target;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initiales Focus-Element setzen
|
// Initiales Focus-Element setzen
|
||||||
|
|
@ -44,7 +56,19 @@ class FocusManager {
|
||||||
autofocusEl.focus();
|
autofocusEl.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Erstes Element im Content bevorzugen (nicht Nav)
|
// Erstes Element im sichtbaren Content-Bereich (Karten bevorzugen)
|
||||||
|
const contentAreas = document.querySelectorAll(
|
||||||
|
".tv-grid, .tv-list-compact, .tv-detail-list, .tv-folder-view, .tv-row, .tv-episode-list"
|
||||||
|
);
|
||||||
|
for (const area of contentAreas) {
|
||||||
|
if (!area.offsetHeight) continue;
|
||||||
|
const firstEl = area.querySelector("[data-focusable]");
|
||||||
|
if (firstEl) {
|
||||||
|
firstEl.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: erstes Content-Element
|
||||||
const contentFirst = document.querySelector(".tv-main [data-focusable]");
|
const contentFirst = document.querySelector(".tv-main [data-focusable]");
|
||||||
if (contentFirst) {
|
if (contentFirst) {
|
||||||
contentFirst.focus();
|
contentFirst.focus();
|
||||||
|
|
@ -87,18 +111,28 @@ class FocusManager {
|
||||||
_navigate(direction, e) {
|
_navigate(direction, e) {
|
||||||
const active = document.activeElement;
|
const active = document.activeElement;
|
||||||
|
|
||||||
// Input-Felder: Links/Rechts nicht abfangen (Cursor-Navigation)
|
// Input-Felder: Nur im Editier-Modus Cursor-Navigation erlauben
|
||||||
if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA")) {
|
if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA")) {
|
||||||
|
if (active.type === "checkbox") {
|
||||||
|
// Checkbox: normal navigieren
|
||||||
|
} else if (this._inputActive) {
|
||||||
|
// Editier-Modus: Links/Rechts fuer Cursor, Hoch/Runter navigiert weg
|
||||||
if (direction === "ArrowLeft" || direction === "ArrowRight") return;
|
if (direction === "ArrowLeft" || direction === "ArrowRight") return;
|
||||||
}
|
}
|
||||||
|
// Nicht aktiv: alle Richtungen navigieren weiter
|
||||||
|
}
|
||||||
|
|
||||||
// Select-Elemente: Nur wenn aktiviert (Enter gedrueckt) Hoch/Runter durchlassen
|
// Select-Elemente: Nur wenn aktiviert (Enter gedrueckt) Hoch/Runter durchlassen
|
||||||
if (active && active.tagName === "SELECT") {
|
if (active && active.tagName === "SELECT") {
|
||||||
if (this._selectActive) {
|
if (this._selectActive) {
|
||||||
// Editier-Modus: Hoch/Runter aendert den Wert
|
// Editier-Modus: Hoch/Runter aendert den Wert
|
||||||
if (direction === "ArrowUp" || direction === "ArrowDown") return;
|
if (direction === "ArrowUp" || direction === "ArrowDown") return;
|
||||||
|
} else {
|
||||||
|
// Nicht im Editier-Modus: native SELECT-Aenderung verhindern
|
||||||
|
if (direction === "ArrowUp" || direction === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Sonst: normal weiternavigieren (Select wird uebersprungen)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusables = this._getFocusableElements();
|
const focusables = this._getFocusableElements();
|
||||||
|
|
@ -129,15 +163,31 @@ class FocusManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Von Nav nach unten -> zum Content springen
|
// Von Nav nach unten -> direkt zu Content-Karten (Filter/View-Switch ueberspringen)
|
||||||
if (inNav && direction === "ArrowDown") {
|
if (inNav && direction === "ArrowDown") {
|
||||||
if (this._lastContentFocus && document.contains(this._lastContentFocus)) {
|
// Gespeicherten Content-Focus bevorzugen (nur wenn noch sichtbar)
|
||||||
|
if (this._lastContentFocus && document.contains(this._lastContentFocus)
|
||||||
|
&& this._lastContentFocus.offsetHeight > 0) {
|
||||||
this._lastContentFocus.focus();
|
this._lastContentFocus.focus();
|
||||||
this._lastContentFocus.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
this._lastContentFocus.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Sonst: erstes Content-Element
|
// Direkt zum sichtbaren Content-Bereich (Karten/Listen-Eintraege)
|
||||||
|
const contentAreas = document.querySelectorAll(
|
||||||
|
".tv-grid, .tv-list-compact, .tv-detail-list, .tv-folder-view, .tv-row, .tv-episode-list"
|
||||||
|
);
|
||||||
|
for (const area of contentAreas) {
|
||||||
|
if (!area.offsetHeight) continue;
|
||||||
|
const firstEl = area.querySelector("[data-focusable]");
|
||||||
|
if (firstEl) {
|
||||||
|
firstEl.focus();
|
||||||
|
firstEl.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: erstes Content-Element
|
||||||
const contentFirst = document.querySelector(".tv-main [data-focusable]");
|
const contentFirst = document.querySelector(".tv-main [data-focusable]");
|
||||||
if (contentFirst) {
|
if (contentFirst) {
|
||||||
contentFirst.focus();
|
contentFirst.focus();
|
||||||
|
|
@ -249,6 +299,22 @@ class FocusManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Input/Textarea: Enter aktiviert/deaktiviert Editier-Modus
|
||||||
|
if ((active.tagName === "INPUT" && active.type !== "checkbox") ||
|
||||||
|
active.tagName === "TEXTAREA") {
|
||||||
|
if (this._inputActive) {
|
||||||
|
// Editier-Modus beenden
|
||||||
|
this._inputActive = false;
|
||||||
|
active.classList.remove("input-editing");
|
||||||
|
} else {
|
||||||
|
// Editier-Modus starten
|
||||||
|
this._inputActive = true;
|
||||||
|
active.classList.add("input-editing");
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Checkbox: Toggle
|
// Checkbox: Toggle
|
||||||
if (active.tagName === "INPUT" && active.type === "checkbox") {
|
if (active.tagName === "INPUT" && active.type === "checkbox") {
|
||||||
active.checked = !active.checked;
|
active.checked = !active.checked;
|
||||||
|
|
@ -267,12 +333,17 @@ class FocusManager {
|
||||||
_goBack(e) {
|
_goBack(e) {
|
||||||
const active = document.activeElement;
|
const active = document.activeElement;
|
||||||
|
|
||||||
// In Input-Feldern: Escape = Blur
|
// In Input-Feldern: Escape = Editier-Modus beenden oder Blur
|
||||||
if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA")) {
|
if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA")) {
|
||||||
if (e.key === "Escape" || e.keyCode === 10009) {
|
if (this._inputActive) {
|
||||||
|
// Editier-Modus beenden, Focus bleibt
|
||||||
|
this._inputActive = false;
|
||||||
|
active.classList.remove("input-editing");
|
||||||
|
} else {
|
||||||
|
// Nicht im Editier-Modus: Focus verlassen
|
||||||
active.blur();
|
active.blur();
|
||||||
e.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue