diff --git a/tables/js/save.js b/tables/js/save.js index d223b94..1cdba94 100644 --- a/tables/js/save.js +++ b/tables/js/save.js @@ -174,7 +174,8 @@ // --- The save itself --------------------------------------------- - async function saveRow(rowId) { + async function saveRow(rowId, opts) { + opts = opts || {}; const { row, drafts } = rowFromState(rowId); if (!row || !drafts || Object.keys(drafts).length === 0) { return { status: 'noop' }; @@ -196,14 +197,20 @@ const headers = { 'Content-Type': 'application/yaml; charset=utf-8' }; if (row.etag) headers['If-Match'] = '"' + row.etag + '"'; + const fetchOpts = { + method: 'PUT', + body: yamlBody, + headers: headers, + credentials: 'same-origin', + }; + // The unload path passes keepalive:true so the PUT outlives the + // page navigation. Subject to the spec's 64 KB body cap — large + // rows may fail in that path; normal saves are unaffected. + if (opts.keepalive) fetchOpts.keepalive = true; + let resp; try { - resp = await fetch(row.yamlUrl, { - method: 'PUT', - body: yamlBody, - headers: headers, - credentials: 'same-origin', - }); + resp = await fetch(row.yamlUrl, fetchOpts); } catch (err) { // Network failure — outbox-fronted client should still // resolve with 202; reaching here means a hard client-side @@ -382,15 +389,13 @@ const drafts = app.state.drafts || {}; const ids = Object.keys(drafts); for (let i = 0; i < ids.length; i++) { - saveRow(ids[i]).catch(() => {}); + saveRow(ids[i], { keepalive: true }).catch(() => {}); } } // Window unload handler — call any in-flight drafts so the user - // doesn't lose typing on tab-close. Best-effort; modern browsers - // limit what beforeunload can do but a fetch with keepalive: true - // gives us one shot. (TODO: switch to keepalive on save for the - // unload path.) + // doesn't lose typing on tab-close. The PUT uses keepalive:true so + // it survives navigation; that comes with a 64 KB body cap. window.addEventListener('beforeunload', function (_ev) { flushAllDrafts(); }); diff --git a/zddc/internal/handler/tables.html b/zddc/internal/handler/tables.html index 8ce2a82..1e4dc80 100644 --- a/zddc/internal/handler/tables.html +++ b/zddc/internal/handler/tables.html @@ -924,6 +924,34 @@ body.help-open .app-header { cursor: not-allowed; } +/* Standalone welcome — shown when form.html is opened directly (no server-injected #form-context). */ +.form-welcome { + max-width: 36rem; + margin: 2rem auto; + padding: 1.5rem 1.75rem; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); +} +.form-welcome h2 { + margin-bottom: 0.5rem; + font-size: 1.25rem; +} +.form-welcome h3 { + margin: 1rem 0 0.35rem; + font-size: 0.95rem; +} +.form-welcome p { margin-bottom: 0.75rem; line-height: 1.5; } +.form-welcome ol { margin: 0 0 0.75rem 1.25rem; } +.form-welcome li { margin-bottom: 0.35rem; } +.form-welcome code { + font-family: var(--font-mono); + font-size: 0.85em; + background: var(--bg-secondary); + padding: 0.05em 0.3em; + border-radius: 3px; +} +
@@ -939,7 +967,7 @@ body.help-open .app-header {This tool renders a form spec injected by zddc-server',
+ ' at <name>.form.html URLs. There is no schema',
+ ' to render here — most likely you opened the standalone HTML directly.
zddc-server against an archive that contains a',
+ ' <name>.form.yaml spec.<path>/<name>.form.html in the browser.See ', + 'zddc.varasys.io/reference.html for the full ZDDC reference.
' + ].join(''); + root.appendChild(wrap); + + const submitBtn = document.getElementById('submit-btn'); + if (submitBtn) submitBtn.hidden = true; + } + function boot() { // When this bundle is hosted by the unified tables.html, the // mode dispatcher decides which app paints. Skip when mode is @@ -5316,6 +5380,12 @@ body.help-open .app-header { app.context.ui || {}, app.context.data ); + } else if (root) { + // No schema — server-injected context is empty. Most common + // when the standalone form.html is opened from file:// without + // a host. Show a friendly explanation instead of a blank page. + renderStandaloneWelcome(root); + return; } if (app.context.errors && app.context.errors.length) {