Put each page's settings on that page
A watchdog threshold was reachable only from the bottom of a list of a hundred and twenty on another tab.
This commit is contained in:
@@ -64,6 +64,42 @@
|
||||
// fatals on the one page that included this without it, which is what took the AI tab blank.
|
||||
require_once __DIR__ . '/confform.php';
|
||||
|
||||
// A whole settings card: disclosure, filter, save, and the fields for one subject. Emitted by a
|
||||
// page in one line rather than assembled there.
|
||||
//
|
||||
// This exists because the alternative was six copies. The AI tab, the Scheduler and the Settings
|
||||
// page each grew their own load/filter/dirty/save cycle, and the third one was written by copying
|
||||
// the second — which is exactly how this plugin ended up with nine settings vocabularies and six
|
||||
// toggles. Everything subtle here is subtle because it already cost something once: the filter
|
||||
// hides rather than removes so a pending edit survives it, the save rebases instead of refetching
|
||||
// so the field under the cursor does not repaint, and a refused write leaves the row dirty
|
||||
// because it genuinely did not happen.
|
||||
//
|
||||
// $prefix composes every id, so a page may hold more than one card
|
||||
// $match which sections — a whole word matched against section headers, or "*" for all
|
||||
// $title the card's heading
|
||||
function vv_conf_ui_card(string $prefix, string $match, string $title = 'Settings'): void {
|
||||
vv_conf_ui_assets();
|
||||
$p = htmlspecialchars($prefix, ENT_QUOTES);
|
||||
?>
|
||||
<div class="vv-cf-card" id="<?= $p ?>-card">
|
||||
<div class="vv-cf-card-h" id="<?= $p ?>-t">
|
||||
<span class="vv-cf-card-c">▶</span>
|
||||
<span class="vv-cf-card-l"><?= htmlspecialchars($title) ?></span>
|
||||
<input type="text" class="vv-cf-card-f" id="<?= $p ?>-filter"
|
||||
placeholder="filter…" style="display:none">
|
||||
<span class="vv-cf-card-s" id="<?= $p ?>-sum"></span>
|
||||
<button class="vv-cf-card-save" id="<?= $p ?>-save" type="button"
|
||||
style="display:none" disabled>Save</button>
|
||||
</div>
|
||||
<div id="<?= $p ?>-body" style="display:none">
|
||||
<div id="<?= $p ?>-fields"><p class="vv-cf-empty">opens when you do</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>VvConfCard(<?= json_encode($prefix) ?>, <?= json_encode($match) ?>);</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
function vv_conf_ui_assets(): void {
|
||||
static $done = false;
|
||||
if ($done) return;
|
||||
@@ -337,6 +373,100 @@ function vv_conf_ui_assets(): void {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// One card, self-contained. Everything a page used to write for itself.
|
||||
window.VvConfCard = function (prefix, match) {
|
||||
const $ = s => document.getElementById(prefix + '-' + s);
|
||||
const API = '/plugins/varaverk/api/confform.php';
|
||||
let loaded = false;
|
||||
|
||||
const dirty = () => {
|
||||
const n = VvConfUI.dirtyCount(prefix + '-fields');
|
||||
$('save').disabled = (n === 0);
|
||||
$('sum').textContent = n ? (n + ' unsaved') : '';
|
||||
$('sum').className = 'vv-cf-card-s' + (n ? ' warn' : '');
|
||||
};
|
||||
|
||||
function load() {
|
||||
if (loaded) return;
|
||||
loaded = true;
|
||||
fetch(API + '?sections=' + encodeURIComponent(match))
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (!d.ok) throw new Error(d.error || 'could not be read');
|
||||
VvConfUI.render(prefix + '-fields', d.groups || []);
|
||||
VvConfUI.wire(prefix + '-fields', dirty);
|
||||
dirty();
|
||||
})
|
||||
.catch(e => {
|
||||
// Retryable — closing and reopening tries again rather than leaving a permanent error.
|
||||
loaded = false;
|
||||
$('fields').innerHTML = '<p class="vv-cf-empty">could not be read: '
|
||||
+ String(e.message || e) + '</p>';
|
||||
});
|
||||
}
|
||||
|
||||
$('t').addEventListener('click', e => {
|
||||
// The filter and the save live in the header; clicking either must not collapse the card.
|
||||
if (e.target.closest('#' + prefix + '-filter, #' + prefix + '-save')) return;
|
||||
const open = $('body').style.display === 'none';
|
||||
$('body').style.display = open ? '' : 'none';
|
||||
$('t').classList.toggle('open', open);
|
||||
$('filter').style.display = open ? '' : 'none';
|
||||
$('save').style.display = open ? '' : 'none';
|
||||
// First open only, so collapsing to look at something else does not discard edits.
|
||||
if (open) load();
|
||||
});
|
||||
|
||||
// Hides whole sections, never individual fields — a setting means little without the heading
|
||||
// saying what it belongs to. display:none rather than removal, so a field edited before
|
||||
// filtering is still dirty and still saved: a filter that silently dropped pending edits
|
||||
// would be a data-loss bug wearing a search box.
|
||||
$('filter').addEventListener('input', () => {
|
||||
const q = $('filter').value.trim().toLowerCase();
|
||||
$('fields').querySelectorAll('.vv-cf-group').forEach(g => {
|
||||
g.classList.toggle('vv-filtered', q !== '' && !g.textContent.toLowerCase().includes(q));
|
||||
});
|
||||
});
|
||||
|
||||
$('save').addEventListener('click', () => {
|
||||
const changes = VvConfUI.collect(prefix + '-fields');
|
||||
if (!changes.length) return;
|
||||
$('save').disabled = true;
|
||||
$('sum').textContent = 'saving…';
|
||||
$('sum').className = 'vv-cf-card-s';
|
||||
|
||||
// URLSearchParams, not FormData — a multipart POST to this plugin's endpoints hangs with
|
||||
// no status ever returned.
|
||||
fetch(API, { method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: new URLSearchParams({ changes: JSON.stringify(changes) }) })
|
||||
.then(r => r.text())
|
||||
.then(t => {
|
||||
let d;
|
||||
try { d = JSON.parse(t); }
|
||||
catch (_) {
|
||||
throw new Error(t.trim() ? 'unparseable response'
|
||||
: 'empty response — rejected before the endpoint ran');
|
||||
}
|
||||
if (!d.ok) throw new Error(d.error || 'save failed');
|
||||
// Rebased, not refetched: the values on screen are the values on disk now, and a
|
||||
// reload would repaint the control under the cursor.
|
||||
VvConfUI.commit(prefix + '-fields');
|
||||
dirty();
|
||||
$('sum').textContent = 'saved ' + changes.length
|
||||
+ ' change' + (changes.length > 1 ? 's' : '');
|
||||
$('sum').className = 'vv-cf-card-s ok';
|
||||
})
|
||||
.catch(e => {
|
||||
// Left dirty on purpose. A refused write changed nothing, and clearing the marks
|
||||
// would say it had.
|
||||
$('sum').textContent = String(e.message || e);
|
||||
$('sum').className = 'vv-cf-card-s bad';
|
||||
dirty();
|
||||
});
|
||||
});
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
|
||||
Reference in New Issue
Block a user