Kundenansicht neu: kein Hineinzoomen beim Tippen auf dem iPhone

Messung am iPhone: die Seite ist verkleinert gezeichnet (462 px Breite
auf 393 pt, visualViewport.scale 0,85, wohl Seitenzoom 85 %). Unsere
16-px-Felder haben auf dem Bildschirm dann nur 13,6 pt, iOS zoomt beim
Antippen auf 100 % hinein und danach nicht zurück: Ausschnitt, rechts
abgeschnitten, die Eingabe liegt hinter der Tastatur.

zoom-ausgleich.js setzt auf iOS bei einem Maßstab unter 0,99 die Klasse
kn-zoom-ausgleich und den Faktor 1,01/scale an <html>; huelle.css gibt
dann allen Textfeldern der Kundenansicht 16 px mal Faktor, also auf dem
Bildschirm wieder 16 pt. Nie umgestellt, während ein Feld den Fokus
hat. Bei 100 % ändert sich nichts (Chromium: pixelgleich zu main).
Die Messung meldet zusätzlich Maßstab (3 Stellen), Faktor, Seitenbreite
und Schriftgröße des Feldes im Fokus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-29 11:30:56 +00:00
1 parent 17bce304eb
commit 1a09bebe41
6 files changed
+99 -5

No files matched your search

+4 -2
View File
@@ -6,7 +6,7 @@ const MAX = 80
const ZIEL = '/api/diag/hoehe' const ZIEL = '/api/diag/hoehe'
const rund = (x) => (Number.isFinite(x) ? Math.round(x * 10) / 10 : null) const rund = (x) => (Number.isFinite(x) ? Math.round(x * 10) / 10 : null)
export function starteDiagHoehe({ win = window, doc = document }) { export function starteDiagHoehe({ win = window, doc = document, zoom = null }) {
const t0 = Date.now() const t0 = Date.now()
let n = 0 let n = 0
const probe = (stil) => { const probe = (stil) => {
@@ -31,7 +31,9 @@ export function starteDiagHoehe({ win = window, doc = document }) {
iw: win.innerWidth, ih: win.innerHeight, ow: win.outerWidth, oh: win.outerHeight, iw: win.innerWidth, ih: win.innerHeight, ow: win.outerWidth, oh: win.outerHeight,
cw: doc.documentElement.clientWidth, ch: doc.documentElement.clientHeight, dh: doc.documentElement.scrollHeight, cw: doc.documentElement.clientWidth, ch: doc.documentElement.clientHeight, dh: doc.documentElement.scrollHeight,
sw: win.screen?.width, sh: win.screen?.height, sah: win.screen?.availHeight, dpr: win.devicePixelRatio, sw: win.screen?.width, sh: win.screen?.height, sah: win.screen?.availHeight, dpr: win.devicePixelRatio,
vv: vv ? [rund(vv.width), rund(vv.height), rund(vv.offsetTop), rund(vv.pageTop), rund(vv.scale)] : null, vv: vv ? [rund(vv.width), rund(vv.height), rund(vv.offsetTop), rund(vv.pageTop), Math.round(vv.scale * 1000) / 1000] : null,
zf: zoom?.faktor?.() ?? null, swd: doc.documentElement.scrollWidth,
fs: aktiv && aktiv !== doc.body ? win.getComputedStyle(aktiv).fontSize : null,
sy: rund(win.scrollY), sy: rund(win.scrollY),
sa: [cs.paddingTop, cs.paddingRight, cs.paddingBottom, cs.paddingLeft].map((p) => rund(parseFloat(p))), sa: [cs.paddingTop, cs.paddingRight, cs.paddingBottom, cs.paddingLeft].map((p) => rund(parseFloat(p))),
vh: [vh.offsetHeight, dvh.offsetHeight, svh.offsetHeight, lvh.offsetHeight], vh: [vh.offsetHeight, dvh.offsetHeight, svh.offsetHeight, lvh.offsetHeight],
+5
View File
@@ -195,6 +195,11 @@
.kn:has(#bereich-chat:not([hidden])) .kn-kopf-titel { opacity: 1; transform: none; } .kn:has(#bereich-chat:not([hidden])) .kn-kopf-titel { opacity: 1; transform: none; }
} }
/* ---------- iOS bei verkleinerter Seite (zoom-ausgleich.js) ----------
Ist die Seite kleiner gezeichnet (Seitenzoom unter 100 %), hätten 16-px-Felder auf dem Bildschirm weniger als 16 pt, und iOS zoomt beim Tippen
hinein und nicht wieder heraus. Die Klasse an <html> setzt nur zoom-ausgleich.js; der Faktor gleicht den Maßstab aus. */
.kn-zoom-ausgleich .kn :is(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="color"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="image"]), textarea, select) { font-size: calc(16px * var(--kn-zoom-ausgleich, 1)) !important; }
/* ---------- Querformat (Handy gedreht): niedrigere Zeile, keine große Überschrift ---------- */ /* ---------- Querformat (Handy gedreht): niedrigere Zeile, keine große Überschrift ---------- */
@media (max-height: 500px) and (max-width: 880px), (max-height: 500px) and (pointer: coarse) { @media (max-height: 500px) and (max-width: 880px), (max-height: 500px) and (pointer: coarse) {
+5 -2
View File
@@ -7,7 +7,8 @@ import { h, leeren, text } from './dom.js'
import { api as apiStandard } from './api.js' import { api as apiStandard } from './api.js'
import * as routerStandard from './router.js' import * as routerStandard from './router.js'
import { BEREICHE, FUNKTIONS_ETAGEN, hashFuerAnsicht, hashFuerRoute, elternVon, titelFuer, zeigtWiderrufsstreifen } from './routen.js' import { BEREICHE, FUNKTIONS_ETAGEN, hashFuerAnsicht, hashFuerRoute, elternVon, titelFuer, zeigtWiderrufsstreifen } from './routen.js'
import { HANDY_ABFRAGE, baueNavigation, beobachteTastatur, leisteSichtbar } from './leiste.js' import { HANDY_ABFRAGE, baueNavigation, beobachteTastatur, istTextfeld, leisteSichtbar } from './leiste.js'
import { beobachteZoom } from './zoom-ausgleich.js'
import { starteDiagHoehe } from './diag-hoehe.js' import { starteDiagHoehe } from './diag-hoehe.js'
import { baueEtage } from './etage.js' import { baueEtage } from './etage.js'
import { erzeugeZaehler } from './zaehler.js' import { erzeugeZaehler } from './zaehler.js'
@@ -278,8 +279,10 @@ export async function starteHuelle({ win = window, doc = document, api = apiStan
if (handyMq.addEventListener) handyMq.addEventListener('change', beiMedien) if (handyMq.addEventListener) handyMq.addEventListener('change', beiMedien)
else handyMq.addListener?.(beiMedien) else handyMq.addListener?.(beiMedien)
beobachteTastatur({ win, doc, onChange: (z) => doc.body.classList.toggle('kn-tastatur', !leisteSichtbar(z)) }) beobachteTastatur({ win, doc, onChange: (z) => doc.body.classList.toggle('kn-tastatur', !leisteSichtbar(z)) })
// iOS: bei verkleinerter Seite die Feldschrift so vergrößern, dass iOS beim Tippen nicht hineinzoomt (zoom-ausgleich.js)
const zoom = istIos() ? beobachteZoom({ win, doc, istTextfeld }) : null
// VORÜBERGEHENDE MESSUNG (2026-10-29, diag-hoehe.js): Bildschirmwerte am iPhone ins Server-Log, wird nach der Klärung entfernt // VORÜBERGEHENDE MESSUNG (2026-10-29, diag-hoehe.js): Bildschirmwerte am iPhone ins Server-Log, wird nach der Klärung entfernt
if (istIos() && istHandy() && !viewAs) starteDiagHoehe({ win, doc }) if (istIos() && istHandy() && !viewAs) starteDiagHoehe({ win, doc, zoom })
// 7. Fristen und Zähler laden parallel und brechen den Start nie ab // 7. Fristen und Zähler laden parallel und brechen den Start nie ab
ladeFristen(api).then((daten) => { fristen = daten; streifen.setze(daten); aktualisiereKonto() }) ladeFristen(api).then((daten) => { fristen = daten; streifen.setze(daten); aktualisiereKonto() })
+38
View File
@@ -0,0 +1,38 @@
/* Kundenansicht neu: iOS zoomt beim Antippen eines Feldes hinein, wenn dessen Schrift auf dem Bildschirm kleiner als 16 pt ist, und zoomt danach
nicht zurück: man sieht nur noch einen Ausschnitt, rechts ist alles abgeschnitten, die Eingabe liegt hinter der Tastatur. Unsere Felder haben am
Handy 16 px. Ist die Seite aber verkleinert gezeichnet (Seitenzoom in Safari unter 100 %; gemessen am iPhone 2026-10-29: 85 %,
visualViewport.scale 0,85), sind das auf dem Bildschirm nur 13,6 pt. Dann bekommen alle Textfelder so viel größere Schrift, dass sie auf dem
Bildschirm wieder 16 pt hat: Klasse kn-zoom-ausgleich und Faktor --kn-zoom-ausgleich an <html>, die Regel steht in huelle.css. Bei 100 % passiert nichts. */
/** Faktor für die Feldschrift beim Maßstab `scale`; 1 = kein Ausgleich (100 % oder hineingezoomt). 1 % Luft gegen Rundung, auf drei Stellen. */
export function ausgleichFaktor(scale) {
if (!Number.isFinite(scale) || scale <= 0 || scale >= 0.99) return 1
return Math.ceil((1.01 / scale) * 1000) / 1000
}
/**
* Stellt den Faktor beim Start und bei jeder Maßstabsänderung (visualViewport resize) ein, nie während ein Textfeld den Fokus hat
* (iOS hat dann schon entschieden). Rückgabe { faktor, beende }.
*/
export function beobachteZoom({ win = window, doc = document, istTextfeld = () => false }) {
const vv = win.visualViewport
if (!vv) return { faktor: () => 1, beende() {} }
const wurzel = doc.documentElement
let faktor = 1
const setze = () => {
if (istTextfeld(doc.activeElement)) return
const neu = ausgleichFaktor(vv.scale)
if (neu === faktor) return
faktor = neu
if (faktor === 1) {
wurzel.classList.remove('kn-zoom-ausgleich')
wurzel.style.removeProperty('--kn-zoom-ausgleich')
} else {
wurzel.style.setProperty('--kn-zoom-ausgleich', String(faktor))
wurzel.classList.add('kn-zoom-ausgleich')
}
}
setze()
vv.addEventListener('resize', setze)
return { faktor: () => faktor, beende() { vv.removeEventListener('resize', setze) } }
}
+1 -1
View File
@@ -16,5 +16,5 @@ test('huelle.css: am Handy hat die Spalte im Chat mindestens volle Höhe (100vh,
test('huelle.js: der alte Höhen-Wächter (Hülle aus- und einblenden) ist weg, die Messung läuft nur am iPhone und nicht in der Mitarbeiter-Ansicht', () => { test('huelle.js: der alte Höhen-Wächter (Hülle aus- und einblenden) ist weg, die Messung läuft nur am iPhone und nicht in der Mitarbeiter-Ansicht', () => {
const js = lies('huelle.js') const js = lies('huelle.js')
assert.doesNotMatch(js, /beobachteHoehe|'\.\/hoehe\.js'/) assert.doesNotMatch(js, /beobachteHoehe|'\.\/hoehe\.js'/)
assert.match(js, /if \(istIos\(\) && istHandy\(\) && !viewAs\) starteDiagHoehe\(\{ win, doc \}\)/) assert.match(js, /if \(istIos\(\) && istHandy\(\) && !viewAs\) starteDiagHoehe\(\{ win, doc, zoom \}\)/)
}) })
+46
View File
@@ -0,0 +1,46 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { ausgleichFaktor, beobachteZoom } from '../public/neu/zoom-ausgleich.js'
import { lies } from './support/knHuelle.js'
test('ausgleichFaktor: 85 % (iPhone 2026-10-29) ergibt 16 px × 1,188 ≈ 16,2 pt auf dem Bildschirm; 100 %, hineingezoomt und Unsinn ergeben 1', () => {
const f = ausgleichFaktor(0.8506)
assert.equal(f, 1.188)
assert.ok(16 * f * 0.8506 >= 16)
assert.equal(ausgleichFaktor(0.5), 2.02)
for (const s of [1, 0.99, 1.5, 0, -1, NaN, undefined]) assert.equal(ausgleichFaktor(s), 1)
})
function aufbau(scale) {
const hoerer = []
const vv = { scale, addEventListener: (t, fn) => hoerer.push(fn), removeEventListener: (t, fn) => hoerer.splice(hoerer.indexOf(fn), 1), feuere: () => hoerer.forEach((fn) => fn()) }
const klassen = new Set(); const stil = {}
const doc = {
activeElement: null,
documentElement: {
classList: { add: (k) => klassen.add(k), remove: (k) => klassen.delete(k) },
style: { setProperty: (k, v) => { stil[k] = v }, removeProperty: (k) => { delete stil[k] } },
},
}
return { vv, doc, klassen, stil, hoerer, win: { visualViewport: vv } }
}
test('beobachteZoom: setzt Klasse und Faktor bei 85 %, nimmt sie bei 100 % weg, ändert nichts während ein Textfeld den Fokus hat', () => {
const t = aufbau(0.8506)
const feld = { feld: true }
const z = beobachteZoom({ win: t.win, doc: t.doc, istTextfeld: (el) => el?.feld === true })
assert.deepEqual([[...t.klassen], t.stil, z.faktor()], [['kn-zoom-ausgleich'], { '--kn-zoom-ausgleich': '1.188' }, 1.188])
t.doc.activeElement = feld; t.vv.scale = 1; t.vv.feuere() // iOS zoomt beim Tippen: nichts umstellen
assert.equal(z.faktor(), 1.188)
t.doc.activeElement = null; t.vv.feuere()
assert.deepEqual([[...t.klassen], t.stil, z.faktor()], [[], {}, 1])
z.beende(); assert.equal(t.hoerer.length, 0)
const ohne = beobachteZoom({ win: {}, doc: t.doc }); assert.equal(ohne.faktor(), 1); ohne.beende()
})
test('huelle.css und huelle.js: Ausgleich nur über die Klasse an <html>, für alle Textfelder der Kundenansicht, nur auf iOS verdrahtet', () => {
const css = lies('huelle.css')
assert.match(css, /\.kn-zoom-ausgleich \.kn :is\(input:not\(\[type="checkbox"\]\)[^{]*, textarea, select\) \{ font-size: calc\(16px \* var\(--kn-zoom-ausgleich, 1\)\) !important; \}/)
const js = lies('huelle.js')
assert.match(js, /const zoom = istIos\(\) \? beobachteZoom\(\{ win, doc, istTextfeld \}\) : null/)
})