Files
Webklar-Kundenbereich/public/neu/hoehe.js
T
ClaudeandClaude Opus 5.5 0fa8fdbbac Kundenansicht neu: Leiste am iPhone wieder ganz unten (iOS-Höhenfehler)
In der App vom Home-Bildschirm bleibt auf iOS nach dem Schließen der
Tastatur die Fensterhöhe oft rund 60 px kleiner (WebKit-Fehler). Leiste
und Chat-Eingabe stehen dann zu hoch, darunter ein leerer Streifen, bis
die App neu startet.

hoehe.js merkt sich die volle Höhe je Breite und blendet die Hülle nach
dem Verlassen eines Feldes, nach einem resize oder wenn die App wieder
vorne ist kurz aus und ein, wenn die Höhe hängt. Das zwingt WebKit zum
Neu-Messen. Scrollstände und Fokus bleiben, höchstens zwei Versuche je
Anlass, nur in der iOS-App vom Home-Bildschirm am Handy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 10:53:04 +00:00

90 lines
4.4 KiB
JavaScript

/* Kundenansicht neu: iOS-Fehler der App vom Home-Bildschirm. Nach dem Schließen der Tastatur bleibt die Fensterhöhe oft kleiner
(WebKit, bekannt seit iOS 17, in iOS 26 wieder da): alles, was unten fest steht (Leiste, Chat-Eingabe), sitzt dann rund 60 px zu hoch,
darunter bleibt ein leerer weißer Streifen, bis die App neu startet. Abhilfe: die Hülle (mindestens volle Fensterhöhe) kurz aus- und
wieder einblenden, das zwingt WebKit, die Höhe neu zu messen. Scrollstände und Fokus bleiben. Nur in der App vom Home-Bildschirm auf
iOS (das entscheidet `aktiv`) und nur, wenn die Höhe wirklich hängt. */
export const LUFT = 4 // so viel darf die Höhe schwanken, ohne dass sie als hängend gilt
export const PRUEF_MS = [150, 600] // nach dem Schließen der Tastatur: früh und noch einmal, wenn die Animation sicher durch ist
export const MAX_VERSUCHE = 2 // je Anlass (Tastatur zu, App wieder vorne); hilft es nicht, bleibt es bis zum nächsten Anlass dabei
/** Reiner Zustand { voll, breite }: größte innere Höhe bei dieser Breite. Ein Breitenwechsel (Drehen, iPad-Fenster) fängt neu an. */
export function hoeheZustand(alt, { hoehe, breite }) {
if (!alt || Math.abs(breite - alt.breite) > 1) return { voll: hoehe, breite }
return { voll: Math.max(alt.voll, hoehe), breite: alt.breite }
}
/** Hängt die Höhe? Kleiner als die größte bei gleicher Breite und kein Textfeld im Fokus (eine offene Tastatur ist kein Hängen). */
export function haengt(zustand, { hoehe, breite, textfeldAktiv }) {
if (!zustand || textfeldAktiv) return false
if (Math.abs(breite - zustand.breite) > 1) return false
return zustand.voll - hoehe > LUFT
}
/**
* Verdrahtung: merkt sich die volle Höhe (resize), prüft nach dem Verlassen eines Feldes (focusout), nach einem resize ohne Textfeld
* und wenn die App wieder nach vorne kommt (visibilitychange), jeweils zu den Zeiten PRUEF_MS. Rückgabe { pruefe, stand, beende }.
*/
export function beobachteHoehe({ win = window, doc = document, element, aktiv = () => true, istTextfeld = () => false }) {
const messe = () => ({ hoehe: win.innerHeight, breite: win.innerWidth })
let z = hoeheZustand(null, messe())
let versuche = 0
let timer = []
const merke = () => { z = hoeheZustand(z, messe()) }
/** Hülle aus- und einblenden, mit synchronem Umbruch dazwischen; vorher gescrollte Kästen, Fensterscroll und Fokus zurücksetzen. */
function heile() {
const fensterX = win.scrollX ?? 0
const fensterY = win.scrollY ?? 0
const staende = []
for (const el of element.querySelectorAll('*')) {
if (el.scrollTop || el.scrollLeft) staende.push([el, el.scrollTop, el.scrollLeft])
}
const fokus = doc.activeElement && doc.activeElement !== doc.body && element.contains(doc.activeElement) ? doc.activeElement : null
const vorher = element.style.display
element.style.display = 'none'
void element.offsetHeight
element.style.display = vorher
void element.offsetHeight
for (const [el, top, left] of staende) { el.scrollTop = top; el.scrollLeft = left }
if (fensterX || fensterY) win.scrollTo(fensterX, fensterY)
if (fokus && doc.activeElement !== fokus) fokus.focus?.({ preventScroll: true })
}
function pruefe() {
if (!aktiv() || !element) return false
const m = messe()
if (!haengt(z, { ...m, textfeldAktiv: istTextfeld(doc.activeElement) })) { merke(); return false }
if (versuche >= MAX_VERSUCHE) return false
versuche += 1
heile()
merke()
return true
}
const warte = (fn, ms) => (win.setTimeout ? win.setTimeout(fn, ms) : setTimeout(fn, ms))
const stoppe = (id) => (win.clearTimeout ? win.clearTimeout(id) : clearTimeout(id))
const plane = () => {
for (const id of timer) stoppe(id)
timer = PRUEF_MS.map((ms) => warte(pruefe, ms))
}
const neuerAnlass = () => { versuche = 0; plane() }
const beiResize = () => { merke(); if (!istTextfeld(doc.activeElement)) plane() }
const beiUnfokus = () => neuerAnlass()
const beiSicht = () => { if (doc.visibilityState === 'visible') neuerAnlass() }
win.addEventListener('resize', beiResize)
doc.addEventListener('focusout', beiUnfokus)
doc.addEventListener('visibilitychange', beiSicht)
return {
pruefe,
stand: () => ({ ...z, versuche }),
beende() {
for (const id of timer) stoppe(id)
win.removeEventListener('resize', beiResize)
doc.removeEventListener('focusout', beiUnfokus)
doc.removeEventListener('visibilitychange', beiSicht)
},
}
}