From 9d63e00632740b9f8831c010bcd07836e385566a Mon Sep 17 00:00:00 2001 From: christian Date: Wed, 21 Oct 2026 21:17:16 -0400 Subject: [PATCH] Browser tab keyboard: open under the field being typed into; Enter hides it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stage TV is portrait, and the keyboard opened at the bottom of the screen — a reach to the floor to type into a search box at the top. - browser-guest-preload.cjs reports the tapped field's position (and which field it is) with the tap, and again when focus moves to another field. - browser-tab.js opens the keyboard directly under that field, centred on it and kept on screen; if there is no room under it, just above it. Focus moving to a different field (Tab, the next memorize blank) moves the keyboard with it. Re-tapping the same field leaves a dragged keyboard where it was. - The keyboard's Enter submits to the page and then hides the keyboard, so search results are not covered. Tapping a field brings it back. Co-Authored-By: Claude Opus 5.5 --- browser-guest-preload.cjs | 20 ++++++++++++++-- browser-tab.js | 49 +++++++++++++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/browser-guest-preload.cjs b/browser-guest-preload.cjs index 528bf82..a997153 100644 --- a/browser-guest-preload.cjs +++ b/browser-guest-preload.cjs @@ -127,6 +127,19 @@ function restoreOsKeyboard(el) { if (v === null) el.removeAttribute('inputmode'); else el.setAttribute('inputmode', v); } +/* + * Where the field is, so the app can open its keyboard right under it: the stage TV is portrait, and a + * keyboard parked at the bottom of a 75" screen is a reach to the floor. `field` tells one field from + * another, so focus moving to a different one (Tab, the next memorize blank) can move the keyboard too. + */ +let fieldSeq = 0; +const fieldIds = new WeakMap(); +function fieldInfo(el) { + if (!fieldIds.has(el)) fieldIds.set(el, ++fieldSeq); + const r = el.getBoundingClientRect(); + return { field: fieldIds.get(el), rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } }; +} + /* * Only a TAP raises the keyboard. A page that focuses its own search box as it loads must not throw a * keyboard over the stage while nobody is touching it; focus moving between fields (Tab, the next @@ -137,10 +150,13 @@ window.addEventListener('pointerdown', (e) => { const el = editableFrom(e.target); if (!el) return; quietOsKeyboard(el); - send('bibleapp:editable', { on: true, tap: true }); + send('bibleapp:editable', Object.assign({ on: true, tap: true }, fieldInfo(el))); }, true); document.addEventListener('focusin', (e) => { - send('bibleapp:editable', { on: isEditable(e.target), tap: false }); + const el = e.target; + if (!isEditable(el)) { send('bibleapp:editable', { on: false, tap: false }); return; } + // Measured a frame later: focusing can scroll the field into view first. + requestAnimationFrame(() => send('bibleapp:editable', Object.assign({ on: true, tap: false }, fieldInfo(el)))); }, true); document.addEventListener('focusout', (e) => { restoreOsKeyboard(e.target); diff --git a/browser-tab.js b/browser-tab.js index fe0d3ae..9c2a4e8 100644 --- a/browser-tab.js +++ b/browser-tab.js @@ -360,7 +360,9 @@ // Only the page on screen may raise or drop the keyboard; a background page focusing its own // search box as it loads must not. if (!d || !isShown(v)) return; - if (d.on && d.tap) showKeyboard(); + if (d.on && d.tap) showKeyboard(v, d); + // Focus moved on to a DIFFERENT field while typing (Tab, the next memorize blank): follow it. + else if (d.on && kb && kb.el.style.display === 'block' && d.field && d.field !== kb.field) placeKeyboard(v, d); else if (!d.on) hideKeyboard(); return; case 'bibleapp:activity': @@ -600,6 +602,9 @@ if (name === 'Enter') v.el.sendInputEvent({ type: 'char', keyCode: '\r' }); v.el.sendInputEvent({ type: 'keyUp', keyCode: code }); } catch (_) {} + // Enter is "done": the search runs and the keyboard gets out of the way of the results. A tap on a + // field brings it back. + if (name === 'Enter') hideKeyboard(); }, focus: function () { var v = activeView(); @@ -616,20 +621,44 @@ el.style.display = 'none'; document.body.appendChild(el); a.createPrayerKeyboard(el, null, 'browser', sink); - kb = { el: el, placed: false }; + kb = { el: el, field: null }; return kb; } - function showKeyboard() { + /* + * The keyboard opens directly under the field being typed into. The stage TV is portrait: anything at + * the bottom of the screen is a reach to the floor, and the page's search box is near the top. If the + * field is too low for the keyboard to fit under it, it goes just above instead. Re-tapping the same + * field while the keyboard is up leaves it wherever it was dragged. + */ + function placeKeyboard(v, d) { + var k = kb; + if (!k || !d || !d.rect || !v || !v.el) return; + var W = v.el.getBoundingClientRect(), z = v.zoom || 1; + var fieldTop = W.top + d.rect.y * z; + var fieldBottom = W.top + (d.rect.y + d.rect.h) * z; + var fieldMid = W.left + (d.rect.x + d.rect.w / 2) * z; + var w = k.el.offsetWidth || 760, h = k.el.offsetHeight || 340, gap = 14; + var y = fieldBottom + gap; + if (y + h > window.innerHeight - 8) { + var above = fieldTop - gap - h; + y = above >= 0 ? above : Math.max(0, window.innerHeight - h - 8); + } + var x = Math.max(0, Math.min(window.innerWidth - w, Math.round(fieldMid - w / 2))); + k.el.style.left = x + 'px'; + k.el.style.top = Math.round(y) + 'px'; + k.field = d.field || null; + } + function showKeyboard(v, d) { if (ROLE !== 'stage') return; var k = keyboard(); if (!k) return; - k.el.style.display = 'block'; - // First time: bottom centre, clear of the page's search box at the top. After that, wherever it was dragged. - if (!k.placed) { - k.placed = true; - var w = k.el.offsetWidth || 760, hgt = k.el.offsetHeight || 340; - k.el.style.left = Math.max(0, Math.round((window.innerWidth - w) / 2)) + 'px'; - k.el.style.top = Math.max(0, Math.round(window.innerHeight - hgt - Math.max(24, window.innerHeight * 0.04))) + 'px'; + var wasHidden = k.el.style.display !== 'block'; + k.el.style.display = 'block'; // shown before measuring: a hidden element has no size + if (d && d.rect && (wasHidden || d.field !== k.field)) placeKeyboard(v, d); + else if (wasHidden && !k.field) { + // No position from the page (should not happen): centre it rather than leave it anywhere odd. + k.el.style.left = Math.max(0, Math.round((window.innerWidth - (k.el.offsetWidth || 760)) / 2)) + 'px'; + k.el.style.top = Math.max(0, Math.round((window.innerHeight - (k.el.offsetHeight || 340)) / 3)) + 'px'; } } function hideKeyboard() { if (kb) kb.el.style.display = 'none'; }