Draw the Scheduler's settings with the same renderer as everything else
It drew its own, so 329 of its 704 fields were text boxes that should have been switches, numbers or lists, and 48 credentials rendered legibly.
This commit is contained in:
@@ -131,17 +131,40 @@ function vv_conf_ui_assets(): void {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// The gutter is seeded here and then kept in step by the input/scroll handlers in wire().
|
||||
// It cannot be produced once and left: the numbers follow the textarea's value, which is the
|
||||
// thing the operator is about to change.
|
||||
function linesCtl(f) {
|
||||
const v = String(f.value == null ? '' : f.value);
|
||||
const n = v.split('\n').length;
|
||||
const gutter = Array.from({ length: n }, (_, i) => i + 1).join('\n');
|
||||
return `<div class="vv-cf-arraywrap">
|
||||
<pre class="vv-cf-lines">${gutter}</pre>
|
||||
<textarea class="vv-cf-array" rows="${Math.min(Math.max(n, 3), 18)}"${attrs(f)}
|
||||
<pre class="vv-cf-lines" aria-hidden="true">${gutterFor(v)}</pre>
|
||||
<textarea class="vv-cf-array" rows="${Math.min(Math.max(n, 3), 20)}"${attrs(f)}
|
||||
data-ctl="text" spellcheck="false">${esc(v)}</textarea>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function gutterFor(v) {
|
||||
const n = String(v).split('\n').length;
|
||||
let s = '';
|
||||
for (let i = 1; i <= n; i++) s += i + '\n';
|
||||
return s;
|
||||
}
|
||||
|
||||
// Kept together, because they are two halves of one illusion: the gutter is a separate element
|
||||
// that has to be renumbered when the text changes and scrolled when the text scrolls, or the
|
||||
// numbers drift away from the lines they belong to.
|
||||
function syncGutter(ta) {
|
||||
const g = ta.parentElement && ta.parentElement.querySelector('.vv-cf-lines');
|
||||
if (!g) return;
|
||||
g.textContent = gutterFor(ta.value);
|
||||
g.scrollTop = ta.scrollTop;
|
||||
}
|
||||
function scrollGutter(ta) {
|
||||
const g = ta.parentElement && ta.parentElement.querySelector('.vv-cf-lines');
|
||||
if (g) g.scrollTop = ta.scrollTop;
|
||||
}
|
||||
|
||||
function textCtl(f) {
|
||||
return `<div class="vv-cf-ctl">
|
||||
<input type="text" class="vv-cf-scalar" value="${escA(f.value)}"${attrs(f)}
|
||||
@@ -152,12 +175,18 @@ function vv_conf_ui_assets(): void {
|
||||
const CONTROLS = { bool: boolCtl, int: intCtl, enum: enumCtl, secret: secretCtl,
|
||||
lines: linesCtl, path: textCtl, text: textCtl };
|
||||
|
||||
function fieldHtml(f) {
|
||||
// n is the running number across the whole panel, not the position within a group. It gives
|
||||
// every setting one unambiguous handle — "number 12" beats "the third one under Docker
|
||||
// Watchdog" when someone is reading it back to you over the phone. The stripe is driven by the
|
||||
// same counter rather than :nth-child, because the group header is also a child and would
|
||||
// throw the parity off inside every section.
|
||||
function fieldHtml(f, n) {
|
||||
const draw = CONTROLS[f.widget] || textCtl;
|
||||
// The description is the conf's own comment. It is the only documentation most of these
|
||||
// settings have, and it is why the form is readable at all.
|
||||
const desc = f.desc ? `<div class="vv-cf-desc">${esc(f.desc)}</div>` : '';
|
||||
return `<div class="vv-cf-field" data-field="${escA(f.key)}">
|
||||
return `<div class="vv-cf-field${n % 2 === 0 ? ' vv-cf-alt' : ''}" data-field="${escA(f.key)}">
|
||||
<span class="vv-cf-num">${n}</span>
|
||||
<span class="vv-cf-dot"></span>
|
||||
<div class="vv-cf-body">
|
||||
<div class="vv-cf-key">${esc(f.key)}</div>
|
||||
@@ -167,8 +196,11 @@ function vv_conf_ui_assets(): void {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function groupHtml(g) {
|
||||
const fields = (g.fields || []).map(fieldHtml).join('');
|
||||
// Takes the counter by reference so numbering continues across groups. Returning it would have
|
||||
// worked too, but every caller would then be responsible for threading it correctly and one of
|
||||
// them eventually would not.
|
||||
function groupHtml(g, counter) {
|
||||
const fields = (g.fields || []).map(f => fieldHtml(f, ++counter.n)).join('');
|
||||
if (!fields) return '';
|
||||
return `<div class="vv-cf-group">
|
||||
<div class="vv-cf-group-header">${esc(g.subsection || g.name || '')}
|
||||
@@ -186,11 +218,32 @@ function vv_conf_ui_assets(): void {
|
||||
}
|
||||
|
||||
window.VvConfUI = {
|
||||
// The markup on its own, for a caller composing it into something larger — the Scheduler's
|
||||
// advanced view wraps it in a Config block alongside the header and README sections. Whoever
|
||||
// uses this owes the DOM a hydrate() afterwards.
|
||||
html(groups) {
|
||||
const counter = { n: 0 };
|
||||
return (groups || []).map(g => groupHtml(g, counter)).join('');
|
||||
},
|
||||
|
||||
// Everything that can only be done once the markup is in the document. Separate from html()
|
||||
// because a caller that built a bigger string still needs this half, and separate from wire()
|
||||
// because wire() is idempotent per container while this must run after every re-render.
|
||||
hydrate(into) {
|
||||
const box = typeof into === 'string' ? document.getElementById(into) : into;
|
||||
if (!box) return;
|
||||
// Seeded from the rendered DOM rather than at build time, so a textarea the browser sized
|
||||
// differently than expected still gets a gutter matching what is on screen.
|
||||
box.querySelectorAll('.vv-cf-array').forEach(syncGutter);
|
||||
return box;
|
||||
},
|
||||
|
||||
render(into, groups) {
|
||||
const box = typeof into === 'string' ? document.getElementById(into) : into;
|
||||
if (!box) return;
|
||||
const html = (groups || []).map(groupHtml).join('');
|
||||
const html = this.html(groups);
|
||||
box.innerHTML = html || '<p class="vv-cf-empty">Nothing configurable here.</p>';
|
||||
this.hydrate(box);
|
||||
return box;
|
||||
},
|
||||
|
||||
@@ -232,7 +285,16 @@ function vv_conf_ui_assets(): void {
|
||||
if (onChange) onChange();
|
||||
};
|
||||
|
||||
box.addEventListener('input', e => { if (e.target.dataset.ctl) refresh(e.target); });
|
||||
box.addEventListener('input', e => {
|
||||
if (!e.target.dataset.ctl) return;
|
||||
if (e.target.classList.contains('vv-cf-array')) syncGutter(e.target);
|
||||
refresh(e.target);
|
||||
});
|
||||
// Capture: scroll does not bubble, so a listener on the container never sees a textarea
|
||||
// scrolling inside it any other way.
|
||||
box.addEventListener('scroll', e => {
|
||||
if (e.target.classList && e.target.classList.contains('vv-cf-array')) scrollGutter(e.target);
|
||||
}, true);
|
||||
box.addEventListener('change', e => {
|
||||
const el = e.target;
|
||||
if (!el.dataset.ctl) return;
|
||||
|
||||
Reference in New Issue
Block a user