openvibers/openvibe.shared Commits

4.141.0: the forms kit loads when a form control is drawn (#329)

OP OpenVibers committed 5 days ago · 8d293a2fc5 · 6 files, +98 −4

* The forms kit loads when a form control is drawn, not merely present

A closed dialog's or a hidden box's select no longer counts: Live's shell carries hidden modal forms, so every Live
page fetched ov-forms.js and .css (17 KB) for selects nobody saw. Hidden controls are watched with an
IntersectionObserver, and the kit loads when one is drawn (a dialog opens, a fold unfolds). A page with a drawn
control loads it once it settles, as before.

* 4.141.0

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex e89fd63..caab51f 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,17 @@
# Changelog
++## 4.141.0 — 2026-11-07
++
++- **The forms kit loads when a form control is drawn, not merely present** (the owner, 11-07: "make things faster and
++ more optimized").
++ - A closed dialog's or a hidden box's select, checkbox or file input no longer counts.
++ - Live's shell carries hidden modal forms, so every Live page fetched `ov-forms.js` and `.css` (17 KB) for
++ controls nobody saw.
++ - Hidden controls are watched with an IntersectionObserver, and the kit loads when one is drawn (a dialog opens, a
++ fold unfolds).
++ - A page with a drawn control loads it once it settles, as before.
++ - Test: `test/forms-when-seen-chrome.test.js`.
++
## 4.140.0 — 2026-11-07
- **Native: no more Material lavender.**
ddiff --git a/README.md b/README.md
iindex 3e2f013..8101f23 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v4.140.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.141.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex df6a605..cf2d660 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,13 +4,14 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.140.0",
++ "version": "4.141.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)",
"sdk": "openvibe-sdk v0.94.0",
"features": [
++ "the forms kit loads when a form control is drawn, not merely present: a closed dialog's select costs nothing (4.141.0)",
"the Compose export maps Material 3's container roles (cards, the navigation bar and its selected pill, dialogs) to Vibe surfaces, so native apps never show Material's lavender (4.140.0)",
"the menu's feed rows with a picture show the picture, not a site icon beside it; a row without one in that group keeps its icon in a picture-sized tile (4.139.0)",
"the footer's shipped line and brand logos load when the footer nears the screen (38 KB off most first visits); the footer, Boost and the glyphs load their files by content hash (4.138.0)",
ddiff --git a/navbar.js b/navbar.js
iindex f723eff..76aab0e 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -584,10 +584,27 @@
// first time one appears. <html data-ov-forms="off"> keeps the browser's own; data-ov-native keeps one control's.
const FORM_CONTROLS = 'select,input[type="checkbox"],input[type="radio"],input[type="range"],input[type="file"]';
let formWatch = null;
++ // Only a control that is drawn counts (4.141.0): a closed dialog's or a hidden box's select does not, so a page whose
++ // shell carries hidden modal forms (Live's) no longer fetches 17 KB it may never use. The hidden ones are watched,
++ // and the kit loads when one is drawn (a dialog opens, a fold unfolds).
++ let formSeen = null;
++ function formsWanted() {
++ const all = document.querySelectorAll(FORM_CONTROLS);
++ if (!all.length) return false;
++ for (let i = 0; i < all.length && i < 200; i++) if (all[i].getClientRects().length) return true;
++ if (typeof IntersectionObserver === 'function') {
++ formSeen = formSeen || new IntersectionObserver((es) => {
++ if (!es.some((e) => e.isIntersecting || e.boundingClientRect.width)) return;
++ formSeen.disconnect(); formSeen = null; loadForms();
++ });
++ for (let i = 0; i < all.length && i < 60; i++) formSeen.observe(all[i]);
++ }
++ return false;
++ }
function loadForms() {
if (root.OVForms || document.getElementById('ov-forms-loader')) return true;
if (document.documentElement.getAttribute('data-ov-forms') === 'off') return true;
-- if (!document.querySelector(FORM_CONTROLS)) return false;
++ if (!formsWanted()) return false;
try {
if (!document.querySelector('link[href*="ov-forms.css"]')) {
const ln = document.createElement('link'); ln.rel = 'stylesheet'; ln.href = siblingV('ov-forms.css'); document.head.appendChild(ln);
ddiff --git a/package.json b/package.json
iindex 2b9c711..9a84d2a 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.140.0",
++ "version": "4.141.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test/forms-when-seen-chrome.test.js b/test/forms-when-seen-chrome.test.js
nnew file mode 100644
iindex 0000000..f6a7921
---- /dev/null
++++ b/test/forms-when-seen-chrome.test.js
@@@ -0,0 +1,64 @@
++'use strict';
++// The forms kit loads when a form control is drawn, not merely present (4.141.0, the owner 11-07: "make things faster
++// and more optimized"). Live's shell carries hidden modal forms, so every Live page fetched ov-forms.js and .css
++// (17 KB) for selects nobody saw. A real headless Chrome; skipped without Chrome.
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const http = require('http');
++
++const ROOT = path.join(__dirname, '..');
++const h = require('../browser-harness');
++if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
++ console.log('forms-when-seen-chrome: skipped (no Chrome; set CHROME_BIN)');
++ process.exit(0);
++}
++
++const page = (body) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Forms</title><link rel="icon" href="data:,">
++<script>window.fetch = () => Promise.resolve({ ok: false, status: 404, json: async () => ({}) });</script>
++<script src="/shared/navbar.js" defer></script></head><body><div id="navbar-mount"></div>${body}
++<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: null, links: [{ label: 'Home', href: '/' }], countViews: false, notifications: false }); });</script>
++</body></html>`;
++const HIDDEN = page('<main><h1>Home</h1><div id="modal" hidden><label>Voice <select id="voice"><option>One</option><option>Two</option></select></label></div></main>');
++const SHOWN = page('<main><h1>Settings</h1><label>Voice <select id="voice"><option>One</option><option>Two</option></select></label></main>');
++
++const server = http.createServer((req, res) => {
++ const u = new URL(req.url, 'http://x');
++ if (u.pathname === '/hidden') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(HIDDEN); }
++ if (u.pathname === '/shown') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(SHOWN); }
++ const m = /^\/shared\/([a-z0-9-]+\.(js|css))$/.exec(u.pathname);
++ if (m && fs.existsSync(path.join(ROOT, m[1]))) { res.writeHead(200, { 'content-type': m[2] === 'css' ? 'text/css' : 'application/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, m[1]))); }
++ res.writeHead(404); return res.end();
++});
++
++(async () => {
++ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
++ const base = `http://127.0.0.1:${server.address().port}`;
++ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-forms-seen-'));
++ const browser = await h.launch({ tmpDir });
++ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
++ try {
++ const context = await browser.newContext();
++ const asked = "performance.getEntriesByType('resource').some((e) => /ov-forms\\.js/.test(e.name))";
++ const until = async (p, expr) => { const end = Date.now() + 4000; while (Date.now() < end) { if (await p.evaluate(expr)) return true; await wait(100); } return false; };
++ const p = await h.openPage(browser, context, { width: 390 });
++ await p.goto(`${base}/hidden`, { maxMs: 5000 });
++ await wait(3000);
++ assert.strictEqual(await p.evaluate(asked), false, 'a hidden dialog\'s select does not fetch the forms kit');
++ assert.strictEqual(await p.evaluate('!!window.OVForms'), false);
++
++ await p.evaluate("(document.getElementById('modal').hidden = false, 1)");
++ assert.ok(await until(p, '!!window.OVForms'), 'the kit loads when the dialog opens');
++ assert.strictEqual(await p.evaluate(asked), true);
++
++ const q = await h.openPage(browser, context, { width: 390 });
++ await q.goto(`${base}/shown`, { maxMs: 5000 });
++ assert.ok(await until(q, '!!window.OVForms'), 'a drawn select loads it once the page settles, as before');
++ console.log('forms-when-seen-chrome: hidden forms fetch nothing; the kit arrives when one is drawn');
++ } finally {
++ await browser.close();
++ server.close();
++ fs.rmSync(tmpDir, { recursive: true, force: true });
++ }
++})().catch((e) => { console.error(e); process.exit(1); });