diff --git a/public/neu/diag-hoehe.js b/public/neu/diag-hoehe.js index 76def16..dbb6e8b 100644 --- a/public/neu/diag-hoehe.js +++ b/public/neu/diag-hoehe.js @@ -6,7 +6,7 @@ const MAX = 80 const ZIEL = '/api/diag/hoehe' 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() let n = 0 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, 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, - 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), sa: [cs.paddingTop, cs.paddingRight, cs.paddingBottom, cs.paddingLeft].map((p) => rund(parseFloat(p))), vh: [vh.offsetHeight, dvh.offsetHeight, svh.offsetHeight, lvh.offsetHeight], diff --git a/public/neu/huelle.css b/public/neu/huelle.css index a985779..35fe0bd 100644 --- a/public/neu/huelle.css +++ b/public/neu/huelle.css @@ -195,6 +195,11 @@ .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 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 ---------- */ @media (max-height: 500px) and (max-width: 880px), (max-height: 500px) and (pointer: coarse) { diff --git a/public/neu/huelle.js b/public/neu/huelle.js index 88022b4..ed29e48 100644 --- a/public/neu/huelle.js +++ b/public/neu/huelle.js @@ -7,7 +7,8 @@ import { h, leeren, text } from './dom.js' import { api as apiStandard } from './api.js' import * as routerStandard from './router.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 { baueEtage } from './etage.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) else handyMq.addListener?.(beiMedien) 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 - 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 ladeFristen(api).then((daten) => { fristen = daten; streifen.setze(daten); aktualisiereKonto() }) diff --git a/public/neu/zoom-ausgleich.js b/public/neu/zoom-ausgleich.js new file mode 100644 index 0000000..0c367fe --- /dev/null +++ b/public/neu/zoom-ausgleich.js @@ -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 , 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) } } +} diff --git a/test/knChatHoehe.test.js b/test/knChatHoehe.test.js index 40637be..e2062ca 100644 --- a/test/knChatHoehe.test.js +++ b/test/knChatHoehe.test.js @@ -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', () => { const js = lies('huelle.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 \}\)/) }) diff --git a/test/knZoomAusgleich.test.js b/test/knZoomAusgleich.test.js new file mode 100644 index 0000000..628d6e8 --- /dev/null +++ b/test/knZoomAusgleich.test.js @@ -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 , 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/) +})