Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3891a027f3 | ||
|
|
e1f541d366 |
+4
-2
@@ -1,6 +1,6 @@
|
||||
# Browser tab
|
||||
|
||||
One web page, full screen in the tab: no address bar, bookmarks or scrollbars — just the page, in the
|
||||
One web page, full screen in the tab: no address bar, bookmarks, scrollbars or focus highlights — just the page, in the
|
||||
same area the Graphics tab uses. It loads when the app starts and keeps running while other tabs are
|
||||
showing, so it is ready the moment it is opened. Built for It Is Written
|
||||
(`https://it-is-written-waters-church.vercel.app/`), but any web page works.
|
||||
@@ -65,7 +65,9 @@ the search and could put different results on the program feed.
|
||||
- **`browser-guest-preload.cjs`** runs inside the page (invisible to the site) and reports what the app
|
||||
cannot see from outside: a tapped text field, a tapped reference (found with the app's own
|
||||
`scripture-scan.js` — the same rules ProPresenter Sync uses), scroll, activity, five fingers, and — only
|
||||
while a follower is linked — a script-free copy of the page's DOM.
|
||||
while a follower is linked — a script-free copy of the page's DOM. It also takes the look (outline,
|
||||
border, glow) out of the site's focus styles: every key on the app's keyboard takes focus from the page
|
||||
for a moment, so a focus highlight would flash with each letter typed.
|
||||
- **`browser-tab.js`** owns the tab: the pages, the keyboard (via `createPrayerKeyboard` with a "sink"),
|
||||
scripture taps, scrolling for the phone / OSC / admin, and the follower's copy. On the follower it
|
||||
shows that copy in sandboxed frames (no scripts); in a plain browser (admin offstage copy, `npm start`)
|
||||
|
||||
@@ -167,6 +167,71 @@ document.addEventListener('focusout', (e) => {
|
||||
}, 0);
|
||||
}, true);
|
||||
|
||||
// ------------------------------------------------------------------ no focus highlight
|
||||
/*
|
||||
* Nothing on the page shows that it has focus. The app's keyboard is outside the page, so every key press
|
||||
* takes focus from the page for a moment and gives it straight back: a site's focus style would flash
|
||||
* with each letter — and on stage it is clutter anyway. Outline rings (the browser's own and any site's)
|
||||
* go with one rule in browser-tab.js's CLEAN_CSS; this takes the rest — a border that brightens, a glow —
|
||||
* out of the site's own :focus / :focus-within / :focus-visible rules, and leaves everything else those
|
||||
* rules do: a menu that opens on focus still opens. Sites add rules as they go (CSS-in-JS, late
|
||||
* stylesheets), so this runs again just before anything can take focus. Another origin's stylesheets
|
||||
* cannot be read and keep theirs; the outline rule still covers their rings.
|
||||
*/
|
||||
const FOCUS_LOOK = /^(?:outline|box-shadow|border)(?:-|$)/;
|
||||
const seenRules = new WeakSet();
|
||||
|
||||
// Split at commas that are not inside brackets or quotes: "a:hover, b:is(c, d)" -> 2; "x:1; y:f(a;b)" -> 2.
|
||||
function splitTop(text, sep) {
|
||||
const out = [];
|
||||
let depth = 0, quote = '', start = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
if (quote) { if (c === '\\') i++; else if (c === quote) quote = ''; continue; }
|
||||
if (c === '"' || c === "'") quote = c;
|
||||
else if (c === '(' || c === '[') depth++;
|
||||
else if (c === ')' || c === ']') depth--;
|
||||
else if (c === sep && depth === 0) { out.push(text.slice(start, i).trim()); start = i + 1; }
|
||||
}
|
||||
out.push(text.slice(start).trim());
|
||||
return out.filter(Boolean);
|
||||
}
|
||||
// Styled BECAUSE it has focus. ":not(:focus)" styles a field at rest, so it does not count.
|
||||
function focusDriven(sel) {
|
||||
return /:focus(?:-within|-visible)?(?![\w-])/.test(sel.replace(/:not\((?:[^()]|\([^()]*\))*\)/g, ''));
|
||||
}
|
||||
function isLook(decl) { return FOCUS_LOOK.test(decl.slice(0, decl.indexOf(':')).trim().toLowerCase()); }
|
||||
|
||||
function stripFocusLook(list, owner) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const r = list[i];
|
||||
if (r.cssRules && r.cssRules.length) stripFocusLook(r.cssRules, r); // @media, @supports, @layer, nesting
|
||||
if (!r.selectorText || !r.style || seenRules.has(r)) continue;
|
||||
seenRules.add(r);
|
||||
const sels = splitTop(r.selectorText, ',');
|
||||
const focus = sels.filter(focusDriven);
|
||||
if (!focus.length) continue;
|
||||
// Declarations as written (cssText), so a shorthand built on var() survives intact.
|
||||
const decls = splitTop(r.style.cssText, ';');
|
||||
const keep = decls.filter((d) => !isLook(d));
|
||||
if (keep.length === decls.length) continue;
|
||||
if (focus.length === sels.length) { r.style.cssText = keep.join('; '); continue; }
|
||||
// Shared with selectors that are not about focus ("a:hover, a:focus"): those keep the whole rule, and
|
||||
// the focus ones move to a copy without the look.
|
||||
r.selectorText = sels.filter((s) => !focusDriven(s)).join(', ');
|
||||
if (!keep.length) continue;
|
||||
try { owner.insertRule(focus.join(', ') + ' { ' + keep.join('; ') + ' }', i + 1); i++; seenRules.add(list[i]); } catch (_) {}
|
||||
}
|
||||
}
|
||||
function hideFocusLook() {
|
||||
const sheets = [...document.styleSheets, ...(document.adoptedStyleSheets || [])];
|
||||
for (const sh of sheets) { try { stripFocusLook(sh.cssRules, sh); } catch (_) {} } // another origin's: unreadable
|
||||
}
|
||||
window.addEventListener('DOMContentLoaded', hideFocusLook);
|
||||
window.addEventListener('load', hideFocusLook);
|
||||
window.addEventListener('pointerdown', hideFocusLook, true);
|
||||
document.addEventListener('focusin', hideFocusLook, true);
|
||||
|
||||
// ------------------------------------------------------------------ activity + five fingers
|
||||
let lastActivity = 0;
|
||||
function activity() {
|
||||
|
||||
+7
-4
@@ -41,15 +41,18 @@
|
||||
var RETRY_MS = 10000;
|
||||
|
||||
/*
|
||||
* The page itself is the only thing on screen: no scrollbars, no blue tap flash, no text selection from
|
||||
* a resting finger, and no rubber-band pull that would drag the whole page (or, sideways, swipe back a
|
||||
* page). Inputs keep their selection so typing still behaves. Applied to the live page with insertCSS
|
||||
* and to the booth's copy as a <style>, so both screens look the same.
|
||||
* The page itself is the only thing on screen: no scrollbars, no blue tap flash, no focus ring, no text
|
||||
* selection from a resting finger, and no rubber-band pull that would drag the whole page (or, sideways,
|
||||
* swipe back a page). Inputs keep their selection so typing still behaves. Applied to the live page with
|
||||
* insertCSS and to the booth's copy as a <style>, so both screens look the same. (The ring matters
|
||||
* twice: every key on the app's keyboard takes focus from the page for a moment, so a ring would flash
|
||||
* with each letter. The site's other focus styles are taken out by browser-guest-preload.cjs.)
|
||||
*/
|
||||
var CLEAN_CSS =
|
||||
'html,body{scrollbar-width:none!important;overscroll-behavior:none!important;}' +
|
||||
'::-webkit-scrollbar{display:none!important;width:0!important;height:0!important;}' +
|
||||
'*{-webkit-tap-highlight-color:transparent!important;}' +
|
||||
':focus,:focus-visible{outline:none!important;}' +
|
||||
'body *{-webkit-user-select:none!important;user-select:none!important;-webkit-touch-callout:none!important;}' +
|
||||
'input,textarea,[contenteditable],[contenteditable] *{-webkit-user-select:text!important;user-select:text!important;}';
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bible-app",
|
||||
"version": "1.4.40",
|
||||
"version": "1.4.41",
|
||||
"description": "Minimalist Bible App for Portrait-Oriented Touchscreen TV",
|
||||
"type": "module",
|
||||
"main": "main.cjs",
|
||||
|
||||
Reference in New Issue
Block a user