Updates group: - Dell drivers and BIOS: check Dell's catalogs against all three driver maps and models.txt, download with a shared speed cap, repack, preview, apply with backups and rollback. - iPXE boot files: check GitHub for the signed Secure Boot set, verify the sha256 and each file's signer, install atomically, roll back. Includes the by-hand procedure. - Blancco: upload a release, stage it, test-boot it from the menu, activate, roll back. - BPRT sync: validate a batch uploaded from the MediaCreator Lite cache (scripts/windows/Sync-BPRT.ps1 or a zip), show the diff, swap with the site overlay files kept. Images: one sidebar entry per image with Overview, Drivers, Unattend and OS and Packages tabs. Every save shows a diff first, keeps the file's format, makes a backup and can be undone. Driver lint runs inline. Enrollment: upload standard/engineer MCL packages, one per tenant/purpose, with archive and restore. Hardware coverage counts engineer as Win11.
513 lines
21 KiB
JavaScript
513 lines
21 KiB
JavaScript
/**
|
|
* unattend-editor.js - the Unattend tab of a GE Aerospace image page
|
|
* (templates/unattend_editor.html). Needs static/image-pages.js (PxeImage,
|
|
* loaded by _image_header.html) and window.PXE_UNATTEND {previewUrl,
|
|
* saveUrl, baseSha}.
|
|
*
|
|
* - command rows: auto-growing textareas, length counters against the
|
|
* schema limits, move up/down, drag to reorder, add, remove;
|
|
* - a filter over both command lists (reordering is off while it is set);
|
|
* - section index that follows the scroll, and marks sections with edits;
|
|
* - Review changes (Ctrl+S): posts the form (or the Raw XML text) to
|
|
* previewUrl, shows the diff, the lint findings and the full XML in the
|
|
* shared review modal, then saves with the previewed file hash.
|
|
*
|
|
* Rows keep data-orig (their index in the file as parsed) so the server
|
|
* can tell a moved or edited item from a new one.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
var CFG = window.PXE_UNATTEND || {};
|
|
|
|
function $(sel, root) { return (root || document).querySelector(sel); }
|
|
function $$(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
|
|
|
|
var LIST_IDS = { spec: 'unaSpec', fl: 'unaFl', driverpaths: 'unaDriverPaths', accounts: 'unaAccounts' };
|
|
var CMD_FIELD = { spec: 'path', fl: 'commandline' };
|
|
|
|
function list(name) { return document.getElementById(LIST_IDS[name]); }
|
|
function rows(name) {
|
|
var l = list(name);
|
|
return l ? $$(':scope > .una-row, :scope > .una-simple', l) : [];
|
|
}
|
|
function origOf(row) {
|
|
return row.hasAttribute('data-orig') ? parseInt(row.getAttribute('data-orig'), 10) : null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Reading the form into the dict services/unattend.apply_form takes
|
|
// ---------------------------------------------------------------------
|
|
function setKey(obj, key, value) {
|
|
var parts = key.split('.');
|
|
if (parts.length === 2) {
|
|
obj[parts[0]] = obj[parts[0]] || {};
|
|
obj[parts[0]][parts[1]] = value;
|
|
} else {
|
|
obj[key] = value;
|
|
}
|
|
}
|
|
|
|
function cmdRow(row, field) {
|
|
var o = { description: $('.una-desc', row).value, orig: origOf(row) };
|
|
o[field] = $('.una-cmd', row).value;
|
|
return o;
|
|
}
|
|
|
|
function collect() {
|
|
var f = {};
|
|
$$('.una-field').forEach(function (el) { setKey(f, el.getAttribute('data-key'), el.value); });
|
|
// A password typed here is plain text: an encoded one (PlainText false
|
|
// in the file) that is replaced must not keep PlainText false.
|
|
var alPass = document.getElementById('unaAlPass');
|
|
if (alPass && f.autologon && alPass.value !== alPass.defaultValue) f.autologon.plain_text = 'true';
|
|
f.driver_paths = rows('driverpaths').map(function (r) {
|
|
return { path: $('.una-dp', r).value, orig: origOf(r) };
|
|
});
|
|
f.user_accounts = rows('accounts').map(function (r) {
|
|
var a = { plain_text: r.getAttribute('data-plain') || 'true', orig: origOf(r) };
|
|
$$('[data-f]', r).forEach(function (i) { a[i.getAttribute('data-f')] = i.value; });
|
|
var pw = $('[data-f="password"]', r);
|
|
if (pw && pw.value !== pw.defaultValue) a.plain_text = 'true';
|
|
return a;
|
|
});
|
|
f.specialize_commands = rows('spec').map(function (r) { return cmdRow(r, 'path'); });
|
|
f.firstlogon_commands = rows('fl').map(function (r) { return cmdRow(r, 'commandline'); });
|
|
return f;
|
|
}
|
|
|
|
// What each index entry / card covers, to mark sections that have edits.
|
|
var SECTIONS = {
|
|
machine: ['computer_name', 'time_zone', 'registered_organization', 'registered_owner'],
|
|
intl: ['intl'], driverpaths: ['driver_paths'], accounts: ['user_accounts'],
|
|
autologon: ['autologon'], oobe: ['oobe'], oobetz: ['oobe_timezone'],
|
|
spec: ['specialize_commands'], fl: ['firstlogon_commands']
|
|
};
|
|
function sectionState(f) {
|
|
var out = {};
|
|
Object.keys(SECTIONS).forEach(function (s) {
|
|
out[s] = JSON.stringify(SECTIONS[s].map(function (k) { return f[k]; }));
|
|
});
|
|
return out;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Row bookkeeping: positions, counts, empty notes, edited marks
|
|
// ---------------------------------------------------------------------
|
|
function rowSnapshot(row) {
|
|
return JSON.stringify($$('input, textarea', row).map(function (i) { return i.value; }));
|
|
}
|
|
|
|
function renumber(name) {
|
|
var rs = rows(name);
|
|
rs.forEach(function (r, i) { var p = $('.una-pos', r); if (p) p.textContent = i + 1; });
|
|
$$('[data-count="' + name + '"]').forEach(function (c) { c.textContent = rs.length; });
|
|
var empty = $('[data-empty="' + name + '"]');
|
|
if (empty) empty.hidden = rs.length > 0;
|
|
}
|
|
|
|
// Length as lint-unattend.py counts it: the escaped text in the file.
|
|
function xmlLength(s) {
|
|
var n = s.length;
|
|
for (var i = 0; i < s.length; i++) {
|
|
var c = s.charAt(i);
|
|
if (c === '&') n += 4; else if (c === '<' || c === '>') n += 3;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
function counter(el, input, limit, always) {
|
|
if (!el || !limit) return;
|
|
var n = xmlLength(input.value);
|
|
var near = n > limit * 0.9;
|
|
el.classList.toggle('near', near && n <= limit);
|
|
el.classList.toggle('over', n > limit);
|
|
el.title = 'Characters as written in the XML (& counts 5, < and > count 4); limit ' + limit + '.';
|
|
if (!always && n < limit * 0.8) { el.textContent = ''; return; }
|
|
if (el.getAttribute('data-for') === 'desc') {
|
|
el.textContent = 'Description ' + n + ' / ' + limit + ' chars' +
|
|
(n > limit ? ': over the limit, Windows rejects the whole pass' : '');
|
|
} else {
|
|
el.textContent = n + ' / ' + limit + (n > limit ? ' over!' : '');
|
|
}
|
|
}
|
|
|
|
function refreshCounters(row) {
|
|
var cmd = $('.una-cmd', row);
|
|
if (!cmd) return;
|
|
var lim = parseInt(cmd.getAttribute('data-limit') || row.parentNode.getAttribute('data-limit'), 10);
|
|
counter($('[data-for="cmd"]', row), cmd, lim, true);
|
|
var desc = $('.una-desc', row);
|
|
counter($('[data-for="desc"]', row), desc, parseInt(desc.getAttribute('data-limit'), 10), false);
|
|
var foot = $('.una-foot', row);
|
|
if (foot) foot.hidden = !foot.textContent.trim();
|
|
}
|
|
|
|
function grow(ta) {
|
|
if (!ta.offsetParent) return; // hidden: sized when shown
|
|
ta.style.height = 'auto';
|
|
ta.style.height = (ta.scrollHeight + 2) + 'px';
|
|
}
|
|
function growAll() { $$('textarea.una-cmd').forEach(grow); }
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Dirty state
|
|
// ---------------------------------------------------------------------
|
|
var raw = document.getElementById('unaRawXml');
|
|
var reviewBtn = document.getElementById('unaReview');
|
|
var initial = '', initialSections = {}, rawInitial = raw ? raw.value : '';
|
|
var saving = false;
|
|
|
|
function formDirty() { return JSON.stringify(collect()) !== initial; }
|
|
function rawDirty() { return raw && raw.value !== rawInitial; }
|
|
|
|
function update() {
|
|
var f = collect();
|
|
var fd = JSON.stringify(f) !== initial;
|
|
var rd = rawDirty();
|
|
var head = document.querySelector('.image-head');
|
|
if (head) head.classList.toggle('dirty', fd || rd);
|
|
if (reviewBtn) reviewBtn.disabled = !(fd || rd);
|
|
var now = sectionState(f);
|
|
var groups = {};
|
|
Object.keys(now).forEach(function (s) {
|
|
var changed = now[s] !== initialSections[s];
|
|
var a = $('.una-index a[href="#sec-' + s + '"]');
|
|
if (!a) return;
|
|
a.classList.toggle('has-edits', changed);
|
|
if (changed) groups[a.getAttribute('data-group')] = true;
|
|
});
|
|
$$('.una-index a.ix-group').forEach(function (a) {
|
|
a.classList.toggle('has-edits', !!groups[a.getAttribute('data-group')]);
|
|
});
|
|
$$('.una-field').forEach(function (el) {
|
|
if (el.type !== 'hidden') el.classList.toggle('una-field-changed', el.value !== el.getAttribute('data-initial'));
|
|
});
|
|
$$('.una-row[data-orig], .una-simple[data-orig]').forEach(function (r) {
|
|
r.classList.toggle('is-edited', rowSnapshot(r) !== r.getAttribute('data-initial'));
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Editing: input, add, remove, move, drag
|
|
// ---------------------------------------------------------------------
|
|
document.addEventListener('input', function (e) {
|
|
var t = e.target;
|
|
if (t.classList && t.classList.contains('una-cmd')) {
|
|
if (/[\r\n]/.test(t.value)) { // one line in the file; a pasted break is a space
|
|
var pos = t.selectionStart;
|
|
t.value = t.value.replace(/[\r\n]+/g, ' ');
|
|
t.setSelectionRange(pos, pos);
|
|
}
|
|
grow(t);
|
|
}
|
|
var row = t.closest && t.closest('.una-row');
|
|
if (row) refreshCounters(row);
|
|
if (t.id === 'unaFilter') applyFilter();
|
|
else scheduleUpdate();
|
|
});
|
|
document.addEventListener('change', function (e) {
|
|
if (e.target.closest && e.target.closest('#unaFormView, #unaRawView')) scheduleUpdate();
|
|
});
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.target.classList && e.target.classList.contains('una-cmd') && e.key === 'Enter') {
|
|
e.preventDefault(); // commands are single lines
|
|
}
|
|
if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
|
|
e.preventDefault();
|
|
if (reviewBtn && !reviewBtn.disabled) review();
|
|
}
|
|
if (e.target.id === 'unaFilter' && e.key === 'Escape') {
|
|
e.target.value = '';
|
|
applyFilter();
|
|
}
|
|
});
|
|
|
|
var pending = null;
|
|
function scheduleUpdate() {
|
|
if (pending) return;
|
|
pending = window.requestAnimationFrame(function () { pending = null; update(); });
|
|
}
|
|
|
|
function listName(row) {
|
|
var l = row.parentNode;
|
|
return l ? l.getAttribute('data-list') : null;
|
|
}
|
|
|
|
function afterChange(name) { renumber(name); scheduleUpdate(); }
|
|
|
|
document.addEventListener('click', function (e) {
|
|
var btn = e.target.closest('.una-up, .una-down, .una-remove, .una-add');
|
|
if (!btn) return;
|
|
if (btn.classList.contains('una-add')) { addRow(btn.getAttribute('data-list')); return; }
|
|
var row = btn.closest('.una-row, .una-simple');
|
|
var name = listName(row);
|
|
if (btn.classList.contains('una-remove')) {
|
|
row.remove();
|
|
} else if (btn.classList.contains('una-up') && row.previousElementSibling) {
|
|
row.parentNode.insertBefore(row, row.previousElementSibling);
|
|
btn.focus();
|
|
} else if (btn.classList.contains('una-down') && row.nextElementSibling) {
|
|
row.parentNode.insertBefore(row.nextElementSibling, row);
|
|
btn.focus();
|
|
}
|
|
afterChange(name);
|
|
});
|
|
|
|
var TPL = { spec: 'unaTplCmd', fl: 'unaTplCmd', driverpaths: 'unaTplDp', accounts: 'unaTplAcct' };
|
|
function addRow(name) {
|
|
var filter = document.getElementById('unaFilter');
|
|
if (filter && filter.value) { filter.value = ''; applyFilter(); }
|
|
var tpl = document.getElementById(TPL[name]);
|
|
var row = tpl.content.firstElementChild.cloneNode(true);
|
|
list(name).appendChild(row);
|
|
var cmd = $('.una-cmd', row);
|
|
if (cmd) {
|
|
cmd.setAttribute('data-limit', list(name).getAttribute('data-limit'));
|
|
grow(cmd);
|
|
refreshCounters(row);
|
|
}
|
|
afterChange(name);
|
|
row.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
var first = $('textarea, input', row);
|
|
if (first) first.focus({ preventScroll: true });
|
|
}
|
|
|
|
// Drag only from the grip, so text in the row stays selectable.
|
|
var dragRow = null;
|
|
document.addEventListener('mousedown', function (e) {
|
|
var grip = e.target.closest('.una-grip');
|
|
if (grip) grip.closest('.una-row').setAttribute('draggable', 'true');
|
|
});
|
|
document.addEventListener('mouseup', function () {
|
|
if (!dragRow) $$('.una-row[draggable]').forEach(function (r) { r.removeAttribute('draggable'); });
|
|
});
|
|
document.addEventListener('dragstart', function (e) {
|
|
var row = e.target.closest && e.target.closest('.una-row');
|
|
if (!row || !row.getAttribute('draggable')) return;
|
|
dragRow = row;
|
|
row.classList.add('dragging');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('text/plain', '');
|
|
});
|
|
function clearDrop() { $$('.drop-before, .drop-after').forEach(function (r) { r.classList.remove('drop-before', 'drop-after'); }); }
|
|
function dropTarget(e) {
|
|
var row = e.target.closest && e.target.closest('.una-row');
|
|
if (!dragRow || !row || row === dragRow || row.parentNode !== dragRow.parentNode) return null;
|
|
var box = row.getBoundingClientRect();
|
|
return { row: row, after: e.clientY > box.top + box.height / 2 };
|
|
}
|
|
document.addEventListener('dragover', function (e) {
|
|
var t = dropTarget(e);
|
|
if (!t) return;
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
clearDrop();
|
|
t.row.classList.add(t.after ? 'drop-after' : 'drop-before');
|
|
});
|
|
document.addEventListener('drop', function (e) {
|
|
var t = dropTarget(e);
|
|
if (!t) return;
|
|
e.preventDefault();
|
|
t.row.parentNode.insertBefore(dragRow, t.after ? t.row.nextSibling : t.row);
|
|
afterChange(listName(dragRow));
|
|
});
|
|
document.addEventListener('dragend', function () {
|
|
if (dragRow) { dragRow.classList.remove('dragging'); dragRow.removeAttribute('draggable'); }
|
|
dragRow = null;
|
|
clearDrop();
|
|
});
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Filter over both command lists
|
|
// ---------------------------------------------------------------------
|
|
function applyFilter() {
|
|
var input = document.getElementById('unaFilter');
|
|
if (!input) return;
|
|
var q = input.value.trim().toLowerCase();
|
|
var shown = 0, total = 0;
|
|
['spec', 'fl'].forEach(function (name) {
|
|
var l = list(name);
|
|
l.classList.toggle('filtering', !!q);
|
|
var hits = 0, rs = rows(name);
|
|
rs.forEach(function (r, i) {
|
|
total++;
|
|
var hay = [$('.una-desc', r).value, $('.una-cmd', r).value,
|
|
($('.una-note', r) || {}).textContent || ''].join(' ').toLowerCase();
|
|
var hit = !q || hay.indexOf(q) >= 0 || q === String(i + 1);
|
|
r.hidden = !hit;
|
|
if (hit) hits++;
|
|
});
|
|
shown += hits;
|
|
var none = $('[data-nomatch="' + name + '"]');
|
|
if (none) none.hidden = !(q && rs.length && !hits);
|
|
});
|
|
var count = document.getElementById('unaFilterCount');
|
|
if (count) {
|
|
count.innerHTML = q
|
|
? '<strong>' + shown + '</strong> of ' + total + ' commands match. Reordering is off while filtering.'
|
|
: total + ' commands';
|
|
}
|
|
if (q) growAll();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Section index follows the scroll
|
|
// ---------------------------------------------------------------------
|
|
// The current section is the last one whose top has passed a line a
|
|
// third of the way down the window; its link (or, in the narrow chip
|
|
// bar, its group's chip) is marked.
|
|
var sections = $$('.una-section');
|
|
var spyQueued = false;
|
|
function spy() {
|
|
spyQueued = false;
|
|
var line = window.innerHeight * 0.33;
|
|
var current = sections[0];
|
|
sections.forEach(function (s) { if (s.getBoundingClientRect().top <= line) current = s; });
|
|
if (!current) return;
|
|
var id = current.id, group = current.getAttribute('data-group');
|
|
$$('.una-index a').forEach(function (a) {
|
|
var on = a.classList.contains('ix-group')
|
|
? a.getAttribute('data-group') === group
|
|
: a.getAttribute('href').slice(1) === id;
|
|
a.classList.toggle('active', on);
|
|
});
|
|
// Narrow bar: when the current entry is not shown (a group heading, or
|
|
// a section folded into its group chip), light the first shown one.
|
|
var shown = $$('.una-index a').filter(function (a) { return a.offsetParent; });
|
|
if (!shown.some(function (a) { return a.classList.contains('active'); })) {
|
|
var alt = shown.filter(function (a) { return a.getAttribute('data-group') === group; })[0];
|
|
if (alt) alt.classList.add('active');
|
|
}
|
|
}
|
|
window.addEventListener('scroll', function () {
|
|
if (!spyQueued) { spyQueued = true; window.requestAnimationFrame(spy); }
|
|
}, { passive: true });
|
|
|
|
var resizeTimer = null;
|
|
window.addEventListener('resize', function () {
|
|
clearTimeout(resizeTimer);
|
|
resizeTimer = setTimeout(function () { growAll(); spy(); }, 150);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Review and save
|
|
// ---------------------------------------------------------------------
|
|
function activeView() {
|
|
var rawPane = document.getElementById('unaRawView');
|
|
return rawPane && rawPane.classList.contains('active') ? 'raw' : 'form';
|
|
}
|
|
|
|
function el(tag, cls, text) {
|
|
var e = document.createElement(tag);
|
|
if (cls) e.className = cls;
|
|
if (text !== undefined) e.textContent = text;
|
|
return e;
|
|
}
|
|
|
|
function lintBlock(lint) {
|
|
var box = el('div', 'una-review-lint');
|
|
if (!lint || !lint.findings || !lint.findings.length) {
|
|
var okLine = el('div', 'una-lint-ok');
|
|
okLine.appendChild(el('i', 'bi bi-check-circle-fill'));
|
|
okLine.appendChild(document.createTextNode('Lint: no findings in the result (the checks of scripts/lint-unattend.py).'));
|
|
box.appendChild(okLine);
|
|
return box;
|
|
}
|
|
var bad = lint.new_errors > 0;
|
|
var a = el('div', 'alert mb-0 ' + (bad ? 'alert-danger' : 'alert-warning'));
|
|
a.appendChild(el('strong', '', bad
|
|
? 'This change adds ' + lint.new_errors + ' lint error' + (lint.new_errors === 1 ? '' : 's') +
|
|
' and will not be saved. One error invalidates the whole answer file for its pass.'
|
|
: 'Lint findings in the result (none added by this change):'));
|
|
var ul = el('ul', 'una-lint-list');
|
|
lint.findings.forEach(function (f) {
|
|
var li = el('li');
|
|
li.appendChild(el('span', 'badge ' + (f.level === 'ERROR' ? 'badge-danger' : 'badge-warning'), f.level));
|
|
if (f['new']) li.appendChild(el('span', 'badge badge-info ms-1', 'new'));
|
|
li.appendChild(document.createTextNode(' ' + f.message));
|
|
ul.appendChild(li);
|
|
});
|
|
a.appendChild(ul);
|
|
box.appendChild(a);
|
|
return box;
|
|
}
|
|
|
|
function review() {
|
|
// The view on screen, unless only the other one has edits.
|
|
var view = activeView();
|
|
var fd = formDirty(), rd = rawDirty();
|
|
var mode = view;
|
|
if (view === 'raw' && !rd && fd) mode = 'form';
|
|
if (view === 'form' && !fd && rd) mode = 'raw';
|
|
var body = { mode: mode, base_sha: CFG.baseSha };
|
|
if (mode === 'form') body.form = collect(); else body.raw_xml = raw.value;
|
|
|
|
var modalEl = document.getElementById('imageReviewModal');
|
|
var out = document.getElementById('imageReviewBody');
|
|
var err = document.getElementById('imageReviewError');
|
|
var confirmBtn = document.getElementById('imageReviewConfirm');
|
|
if (!modalEl || !window.bootstrap || !window.PxeImage) return;
|
|
document.getElementById('imageReviewTitle').textContent =
|
|
'Review changes: FlatUnattendW10.xml (' + (mode === 'form' ? 'Form' : 'Raw XML') + ')';
|
|
out.innerHTML = '<div class="text-muted py-3">Building the XML the ' +
|
|
(mode === 'form' ? 'form' : 'Raw XML tab') + ' would save...</div>';
|
|
err.classList.add('d-none');
|
|
confirmBtn.disabled = true;
|
|
bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
|
|
|
function fail(msg) { err.textContent = msg; err.classList.remove('d-none'); }
|
|
|
|
PxeImage.postJSON(CFG.previewUrl, body).then(function (r) {
|
|
if (!r.ok) { out.innerHTML = ''; fail(r.data.error || 'Preview failed.'); return; }
|
|
var n = PxeImage.renderDiff(out, r.data.files);
|
|
if ((mode === 'form' && rd) || (mode === 'raw' && fd)) {
|
|
out.insertBefore(el('div', 'alert alert-info',
|
|
(mode === 'form' ? 'Your Raw XML edits are' : 'Your Form edits are') +
|
|
' not part of this save and are lost when the page reloads after it.'), out.firstChild);
|
|
}
|
|
out.appendChild(lintBlock(r.data.lint));
|
|
if (n) {
|
|
var det = el('details', 'una-review-xml');
|
|
det.appendChild(el('summary', '', 'Show the whole file as it will be written'));
|
|
det.appendChild(el('pre', 'mono', r.data.xml));
|
|
out.appendChild(det);
|
|
}
|
|
var expect = {};
|
|
(r.data.files || []).forEach(function (f) { expect[f.name] = f.sha_before; });
|
|
confirmBtn.disabled = n === 0 || (r.data.lint && r.data.lint.new_errors > 0);
|
|
confirmBtn.onclick = function () {
|
|
confirmBtn.disabled = true;
|
|
PxeImage.postJSON(CFG.saveUrl, Object.assign({}, body, { expect: expect })).then(function (s) {
|
|
if (!s.ok) { fail(s.data.error || 'Save failed.'); confirmBtn.disabled = s.status === 409; return; }
|
|
saving = true;
|
|
window.location.reload();
|
|
}).catch(function (e) { fail('Save failed: ' + e); confirmBtn.disabled = false; });
|
|
};
|
|
}).catch(function (e) { out.innerHTML = ''; fail('Preview failed: ' + e); });
|
|
}
|
|
|
|
if (reviewBtn) reviewBtn.addEventListener('click', review);
|
|
|
|
window.addEventListener('beforeunload', function (e) {
|
|
if (!saving && (formDirty() || rawDirty())) { e.preventDefault(); e.returnValue = ''; }
|
|
});
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Start
|
|
// ---------------------------------------------------------------------
|
|
$$('.una-field').forEach(function (f) { f.setAttribute('data-initial', f.value); });
|
|
$$('.una-row[data-orig], .una-simple[data-orig]').forEach(function (r) {
|
|
r.setAttribute('data-initial', rowSnapshot(r));
|
|
});
|
|
$$('.una-row').forEach(refreshCounters);
|
|
initial = JSON.stringify(collect());
|
|
initialSections = sectionState(collect());
|
|
growAll();
|
|
applyFilter();
|
|
spy();
|
|
var formTab = document.getElementById('unaFormTab');
|
|
if (formTab) formTab.addEventListener('shown.bs.tab', function () { growAll(); spy(); });
|
|
if (document.fonts && document.fonts.ready) document.fonts.ready.then(growAll);
|
|
})();
|