Der Fix 0fa8fdb hat die zu hoch stehende Leiste am iPhone nicht
behoben. Damit wir nicht weiter raten: die Seite schickt am iPhone
Bildschirmwerte (Fenster, visualViewport, Bildschirm, Safe-Area,
vh-Einheiten, Lage der Leiste, Eingriffe von hoehe.js) an
/api/diag/hoehe, der Server schreibt sie als Zeile "[diag-hoehe]" ins
Log. Keine Inhalte, nur Pilot-Kunden (nicht die Mitarbeiter-Ansicht),
höchstens 80 Meldungen je Seite und 400 Zeilen je Kunde und Stunde.
Wird nach der Klärung wieder entfernt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
4.7 KiB
JavaScript
93 lines
4.7 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. onHeile({ vorher, nachher, voll, versuche }) nach
|
|
* jedem Eingriff. Rückgabe { pruefe, stand, beende }.
|
|
*/
|
|
export function beobachteHoehe({ win = window, doc = document, element, aktiv = () => true, istTextfeld = () => false, onHeile = () => {} }) {
|
|
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
|
|
const vorher = win.innerHeight
|
|
heile()
|
|
merke()
|
|
try { onHeile({ vorher, nachher: win.innerHeight, voll: z.voll, versuche }) } catch { /* nur Beobachter */ }
|
|
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)
|
|
},
|
|
}
|
|
}
|