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>
118 lines
6.2 KiB
JavaScript
118 lines
6.2 KiB
JavaScript
/* Kundenansicht neu: Navigation der Hülle. Handy: Fahrstuhl-Zeile unten mit vier Wörtern und schwarzem Balken (reiter.js, art 'balken').
|
|
Desktop: Menü links, Wörter ohne Icons. Beide zeigen dieselben Zähler. Dazu der reine Zustand der Bildschirmtastatur
|
|
(Leiste und Streifen verschwinden nur bei Texteingabe) und seine Verdrahtung mit visualViewport, focusin und focusout. */
|
|
|
|
import { h, leeren } from './dom.js'
|
|
import { baueReiter, anzeigeText } from './reiter.js'
|
|
|
|
// Genau einmal in JS; das CSS führt dieselbe Abfrage (basis.css, huelle.css).
|
|
export const HANDY_ABFRAGE = '(max-width: 880px), (pointer: coarse) and (max-height: 500px)'
|
|
|
|
export const WOERTER = [
|
|
{ key: 'website', label: 'Website' },
|
|
{ key: 'funktionen', label: 'Funktionen' },
|
|
{ key: 'chat', label: 'Chat', einheit: 'ungelesen' },
|
|
{ key: 'vertraege', label: 'Verträge', einheit: 'offen', ausrufText: 'Zahlung fehlgeschlagen' },
|
|
]
|
|
|
|
const beschriftung = (wort, anzeige) => {
|
|
const text = anzeigeText(anzeige)
|
|
if (!text) return null
|
|
if (anzeige.ausruf) return `${wort.label}, ${wort.ausrufText ?? 'Hinweis'}`
|
|
return `${wort.label}, ${text}${wort.einheit ? ` ${wort.einheit}` : ''}`
|
|
}
|
|
|
|
/** Fahrstuhl-Zeile in `haupt` und Menü in `menue` bauen. onWahl(bereich) wird bei Klick auf ein Wort gerufen. */
|
|
export function baueNavigation({ haupt, menue, onWahl }) {
|
|
const reiter = baueReiter({ eintraege: WOERTER, aktiv: '', art: 'balken', ariaLabel: 'Bereiche', bei: (key) => onWahl(key) })
|
|
leeren(haupt).append(reiter)
|
|
|
|
const knoepfe = Object.fromEntries(WOERTER.map((w) => [w.key, h('button', { class: 'kn-nav-item', type: 'button', 'data-bereich': w.key, on: { click: () => onWahl(w.key) } }, h('span', { class: 'kn-nav-text' }, w.label))]))
|
|
leeren(menue).append(h('span', { class: 'kn-marke' }, 'WEBklar'), ...WOERTER.map((w) => knoepfe[w.key]))
|
|
|
|
function setzeAktiv(bereich) {
|
|
reiter.setzeAktiv(bereich)
|
|
for (const w of WOERTER) {
|
|
if (w.key === bereich) knoepfe[w.key].setAttribute('aria-current', 'page')
|
|
else knoepfe[w.key].removeAttribute('aria-current')
|
|
}
|
|
}
|
|
|
|
function zeigeZahl(wort, anzeige) {
|
|
reiter.setzeAnzeige(wort.key, anzeige)
|
|
const knopf = knoepfe[wort.key]
|
|
let zahl = knopf.querySelector('.kn-nav-zahl')
|
|
const text = anzeigeText(anzeige)
|
|
if (!text) { zahl?.remove(); knopf.removeAttribute('aria-label'); return }
|
|
if (!zahl) { zahl = h('span', { class: 'kn-nav-zahl' }); knopf.append(zahl) }
|
|
zahl.textContent = text
|
|
zahl.classList.toggle('kn-nav-zahl--heiss', wort.key === 'chat' || anzeige.ausruf === true)
|
|
knopf.setAttribute('aria-label', beschriftung(wort, anzeige))
|
|
}
|
|
|
|
function setzeZaehler({ chat = 0, vertraege = { zahl: 0, ausruf: false } } = {}) {
|
|
zeigeZahl(WOERTER[2], { zahl: chat, ausruf: false })
|
|
zeigeZahl(WOERTER[3], { zahl: vertraege.zahl ?? 0, ausruf: vertraege.ausruf === true })
|
|
}
|
|
|
|
/** Etage 2 offen: das Reel (Elternelement der Hauptzeile) fährt eine Zeile hoch, die Hauptzeile ist gesperrt. */
|
|
function setzeEtage(offen) {
|
|
const reel = haupt.parentElement
|
|
if (offen) { reel?.setAttribute('data-etage', 'offen'); haupt.setAttribute('inert', '') }
|
|
else { reel?.removeAttribute('data-etage'); haupt.removeAttribute('inert') }
|
|
}
|
|
|
|
return { setzeAktiv, setzeZaehler, setzeEtage }
|
|
}
|
|
|
|
const SCHWELLE = 150 // so viel kleiner als die volle Höhe heißt: Tastatur offen (wie app.js:2475)
|
|
const TEXT_FELD = 'textarea, input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=date]):not([type=button]):not([type=submit]):not([type=range]):not([type=color])'
|
|
|
|
/** Reiner Zustand { voll, breite, offen, tippt }. Ereignisse: { art:'vv', hoehe, breite, textfeldAktiv?, grob? }, { art:'fokus', textfeld, grob }, { art:'unfokus', textfeldAktiv }. */
|
|
export function tastaturZustand(alt, ereignis) {
|
|
if (ereignis.art === 'fokus') return { ...alt, tippt: Boolean(ereignis.textfeld && ereignis.grob) }
|
|
if (ereignis.art === 'unfokus') return { ...alt, tippt: alt.tippt && Boolean(ereignis.textfeldAktiv) }
|
|
// visualViewport: ein Breitenwechsel (Drehen, Zoom) setzt die volle Höhe zurück, sonst wächst sie mit
|
|
const gedreht = Math.abs(ereignis.breite - alt.breite) > 1
|
|
const voll = gedreht ? ereignis.hoehe : Math.max(alt.voll, ereignis.hoehe)
|
|
const offen = gedreht ? false : ereignis.hoehe < voll - SCHWELLE
|
|
let tippt = alt.tippt
|
|
if (alt.offen && !offen) tippt = false // Tastatur ging zu, auch per Android-Geste ohne Blur
|
|
else if (offen && ereignis.textfeldAktiv && ereignis.grob) tippt = true
|
|
return { voll, breite: gedreht ? ereignis.breite : alt.breite, offen, tippt }
|
|
}
|
|
|
|
/** Steht der Fokus in einem Textfeld (das die Bildschirmtastatur öffnet)? */
|
|
export const istTextfeld = (el) => Boolean(el?.matches?.(TEXT_FELD))
|
|
|
|
/** Leiste und Streifen sind nur bei Texteingabe weg. */
|
|
export const leisteSichtbar = (zustand) => !zustand.tippt
|
|
|
|
/** Verdrahtung mit visualViewport, focusin und focusout; onChange(zustand) nur bei Änderung von offen oder tippt. */
|
|
export function beobachteTastatur({ win = window, doc = document, onChange }) {
|
|
const vv = win.visualViewport
|
|
const grob = () => Boolean(win.matchMedia?.('(pointer: coarse)').matches)
|
|
let z = { voll: vv?.height ?? win.innerHeight, breite: vv?.width ?? win.innerWidth, offen: false, tippt: false }
|
|
const wende = (ereignis) => {
|
|
const alt = z
|
|
z = tastaturZustand(alt, ereignis)
|
|
if (z.offen !== alt.offen || z.tippt !== alt.tippt) onChange(z)
|
|
}
|
|
const beiFokus = (ev) => wende({ art: 'fokus', textfeld: istTextfeld(ev.target), grob: grob() })
|
|
// Nach dem Blur kurz warten: wechselt der Fokus von Feld zu Feld, bleibt die Tastatur
|
|
const warte = (fn, ms) => (win.setTimeout ? win.setTimeout(fn, ms) : setTimeout(fn, ms))
|
|
const beiUnfokus = () => warte(() => wende({ art: 'unfokus', textfeldAktiv: istTextfeld(doc.activeElement) }), 80)
|
|
const beiVv = () => wende({ art: 'vv', hoehe: vv.height, breite: vv.width, textfeldAktiv: istTextfeld(doc.activeElement), grob: grob() })
|
|
doc.addEventListener('focusin', beiFokus)
|
|
doc.addEventListener('focusout', beiUnfokus)
|
|
vv?.addEventListener('resize', beiVv)
|
|
return {
|
|
stand: () => z,
|
|
beende() {
|
|
doc.removeEventListener('focusin', beiFokus)
|
|
doc.removeEventListener('focusout', beiUnfokus)
|
|
vv?.removeEventListener('resize', beiVv)
|
|
},
|
|
}
|
|
}
|