Title the info banner for its view, and number and stripe the array fields

This commit is contained in:
Gmer4Lfe
2026-08-05 18:13:04 -04:00
parent 99486b1eaf
commit eacea56d9d
2 changed files with 76 additions and 8 deletions
+59 -4
View File
@@ -2029,6 +2029,7 @@ function vvEditConf(id) {
cf.innerHTML = (!d.ok || !d.groups || d.groups.length === 0)
? '<p class="vv-cf-empty">No configurable settings found for this host.</p>'
: vvRenderConfForm(d.groups);
vvCfInitArrays(cf);
requestAnimationFrame(vvFitRight);
})
.catch(() => { cf.innerHTML = '<p class="vv-cf-empty">Failed to load configuration.</p>'; });
@@ -2059,10 +2060,18 @@ function vvRenderConfForm(groups) {
+ ' data-key="' + esc(f.key) + '" data-file="' + esc(f.file) + '" data-type="scalar"'
+ ' value="' + esc(f.value) + '">';
} else {
// Arrays get a numbered gutter and striped rows. DAILY_MAINTENANCE_SCRIPTS is 25 lines
// of script paths and interleaved comment blocks; as a bare textarea it reads as one
// block of text and you cannot tell where an entry ends. The stripes do the separating
// and the numbers give each line something to be referred to by.
const rows = Math.min(20, (f.value.match(/\n/g) || []).length + 3);
html += '<textarea class="vv-cf-input vv-cf-array"'
html += '<div class="vv-cf-arraywrap">'
+ '<pre class="vv-cf-lines" aria-hidden="true"></pre>'
+ '<textarea class="vv-cf-input vv-cf-array"'
+ ' data-key="' + esc(f.key) + '" data-file="' + esc(f.file) + '" data-type="' + esc(f.type) + '"'
+ ' rows="' + rows + '">' + esc(f.value) + '</textarea>';
+ ' oninput="vvCfLines(this)" onscroll="vvCfLineScroll(this)"'
+ ' rows="' + rows + '">' + esc(f.value) + '</textarea>'
+ '</div>';
}
html += '</div></div>';
}
@@ -2071,6 +2080,28 @@ function vvRenderConfForm(groups) {
return html;
}
// Line numbers for an array field. The gutter is a plain <pre> scrolled in step with the
// textarea — same shape as the main editor's gutter, minus the highlight overlay, because these
// stay ordinary editable textareas and nothing here needs syntax colouring.
function vvCfLines(ta) {
const g = ta.parentElement.querySelector('.vv-cf-lines');
if (!g) return;
const n = ta.value.split('\n').length;
let s = '';
for (let i = 1; i <= n; i++) s += i + '\n';
g.textContent = s;
g.scrollTop = ta.scrollTop;
}
function vvCfLineScroll(ta) {
const g = ta.parentElement.querySelector('.vv-cf-lines');
if (g) g.scrollTop = ta.scrollTop;
}
// Numbers cannot be produced server-side: the count follows the textarea's value, which the user
// is about to change. Called once after any render that can contain conf fields.
function vvCfInitArrays(root) {
(root || document).querySelectorAll('.vv-cf-array').forEach(vvCfLines);
}
function vvSaveConf() {
if (!vvConfId) return;
const _cfName = vvConfId.replace(/\.sh$/, '').split('/').pop();
@@ -3796,6 +3827,29 @@ function vvSelectScript(row) {
vvShowScriptInfoMode(name, hdr, id);
}
// Retitle the banner for this view: "Daily Sync Maintenance" → "Daily Sync Maintenance Info &
// Settings", re-centred so the = rules still line up. Done at display time and never to the file
// — that banner is the script's own header, it is parsed by include/scheduler.php for the
// library listing, and it belongs to the script rather than to this panel.
//
// Matches only the titled line: the plain rules above and below have no text between their =
// runs, so the pattern cannot hit them.
function vvSiRetitle(hdr, suffix) {
const lines = String(hdr || '').split('\n');
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/^(\s*)(=+)\s+(\S.*?)\s+(=+)\s*$/);
if (!m) continue;
const indent = m[1];
const width = lines[i].trimEnd().length - indent.length;
const title = ' ' + m[3].trim() + ' ' + suffix + ' ';
const pad = Math.max(2, width - title.length);
const left = Math.floor(pad / 2);
lines[i] = indent + '='.repeat(left) + title + '='.repeat(pad - left);
break;
}
return lines.join('\n');
}
function vvShowScriptInfoMode(name, hdr, id) {
document.getElementById('vv-suggestions').style.display = 'none';
document.getElementById('vv-log-pre').style.display = 'none';
@@ -3849,7 +3903,7 @@ function vvShowScriptInfoMode(name, hdr, id) {
if (dispHdr) {
html += '<div class="vv-sinfo-block">'
+ '<pre class="vv-si-hdr">' + vvHl(dispHdr, true) + '</pre>'
+ '<pre class="vv-si-hdr">' + vvHl(vvSiRetitle(dispHdr, 'Info & Settings'), true) + '</pre>'
+ '</div>';
}
@@ -3873,12 +3927,13 @@ function vvShowScriptInfoMode(name, hdr, id) {
}
si.innerHTML = html || '<p class="vv-cf-empty">No additional information found.</p>';
vvCfInitArrays(si);
requestAnimationFrame(vvFitRight);
}).catch(() => { si.innerHTML = '<p class="vv-cf-empty">Failed to load info.</p>'; });
} else {
// Basic mode: header only
si.innerHTML = '<pre class="vv-si-hdr">' + vvHl(hdr, true) + '</pre>';
si.innerHTML = '<pre class="vv-si-hdr">' + vvHl(vvSiRetitle(hdr, 'Info & Settings'), true) + '</pre>';
}
requestAnimationFrame(vvFitRight);
}