profiler editor: keep record/content fields visible

- Seed one empty record row when record content is created or reset
  (filetype change or Contents None->Exact/Mask) via profilerEmptyRecordContent
- Add per-record delete and '+ Record' add controls so the record view is
  always editable; delete is hidden on the last remaining row

SW cache v47.
This commit is contained in:
2026-09-09 23:46:04 +03:00
parent 569f308cec
commit 30969f8f04
3 changed files with 41 additions and 5 deletions
+31 -3
View File
@@ -6744,6 +6744,11 @@ function profilerContentKindForFileType(ft) {
return (ft === 'linear_fixed' || ft === 'cyclic') ? 'record' : 'transparent';
}
// A fresh record content object seeded with one empty record row.
function profilerEmptyRecordContent(mode) {
return { mode: mode, kind: 'record', records: [{ num: 1, data: '' }] };
}
function profilerValidateProfile(obj) {
if (!obj || typeof obj !== 'object') return 'Not an object';
if (!obj.name) return 'Missing name';
@@ -7065,9 +7070,14 @@ function profilerRenderContentEditor(i, r) {
for (let ri = 0; ri < c.records.length; ri++) {
const rec = c.records[ri];
html += '<div class="flex items-center gap-2"><span class="text-xs text-gray-500 w-8">' + rec.num + '</span>' +
'<input value="' + escHtml(rec.data) + '" oninput="profilerUpdateRecord(' + i + ',' + ri + ',this.value)" class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800"></div>';
'<input value="' + escHtml(rec.data) + '" oninput="profilerUpdateRecord(' + i + ',' + ri + ',this.value)" class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800">';
if (c.records.length > 1) {
html += '<button onclick="profilerDeleteRecord(' + i + ',' + ri + ')" class="text-red-400 hover:text-red-600 text-xs px-1"></button>';
}
html += '</div>';
}
html += '</div>';
html += '<button onclick="profilerAddRecord(' + i + ')" class="mt-1 text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300">+ Record</button>';
} else {
html += '<textarea oninput="profilerUpdateRuleContent(' + i + ',this.value)" rows="2" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800">' + escHtml(c.expected || '') + '</textarea>';
}
@@ -7091,7 +7101,7 @@ function profilerUpdateRuleFileType(i, value) {
const kind = profilerContentKindForFileType(value);
if (r.content.kind !== kind) {
r.content = kind === 'record'
? { mode: r.content.mode, kind: 'record', records: [] }
? profilerEmptyRecordContent(r.content.mode)
: { mode: r.content.mode, kind: 'transparent', expected: '' };
}
}
@@ -7108,7 +7118,7 @@ function profilerSetContent(i, mode) {
} else {
const kind = profilerContentKindForFileType(r.fileType);
r.content = kind === 'record'
? { mode: mode, kind: 'record', records: [] }
? profilerEmptyRecordContent(mode)
: { mode: mode, kind: 'transparent', expected: '' };
}
profilerRenderEditor();
@@ -7126,6 +7136,24 @@ function profilerUpdateRecord(i, ri, value) {
if (c && c.records) c.records[ri].data = value;
}
function profilerAddRecord(i) {
if (!profilerDraft) return;
const c = profilerDraft.rules[i].content;
if (!c || c.kind !== 'record') return;
const lastNum = c.records.length ? c.records[c.records.length - 1].num : 0;
c.records.push({ num: lastNum + 1, data: '' });
profilerRenderEditor();
}
function profilerDeleteRecord(i, ri) {
if (!profilerDraft) return;
const c = profilerDraft.rules[i].content;
if (!c || c.kind !== 'record') return;
if (c.records.length <= 1) return;
c.records.splice(ri, 1);
profilerRenderEditor();
}
function profilerSaveProfile() {
if (!profilerDraft) return;
profilerDraft.name = document.getElementById('profiler-name').value.trim();
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v46';
const CACHE = 'otaman-v47';
const URLS = [
'index.html',
'help.html',
+9 -1
View File
@@ -21,7 +21,7 @@ function extractFunc(src, name) {
return src.slice(m.index, i + 1);
}
const FNS = ['profilerNormHex', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerValidateProfile'];
const FNS = ['profilerNormHex', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
eval(code);
@@ -92,6 +92,14 @@ test('profilerContentKindForFileType', () => {
assert.strictEqual(profilerContentKindForFileType('df'), 'transparent');
});
test('profilerEmptyRecordContent seeds one empty record row', () => {
const c = profilerEmptyRecordContent('exact');
assert.strictEqual(c.kind, 'record');
assert.strictEqual(c.mode, 'exact');
assert.strictEqual(c.records.length, 1);
assert.deepStrictEqual(c.records[0], { num: 1, data: '' });
});
test('profile validation', () => {
assert.strictEqual(profilerValidateProfile(null), 'Not an object');
assert.strictEqual(profilerValidateProfile({ name: 'x' }), 'Missing rules array');