ZDDC/transmittal/js/state.js
ZDDC ea385b5366 Initial commit
ZDDC — Zero Day Document Control. A file-naming convention plus five
single-file HTML tools (archive, transmittal, classifier, mdedit,
landing) and an optional Go HTTP server (zddc-server) with ACL and a
virtual archive index. Self-contained, offline-capable, dependency-free.

See README.md for an overview, AGENTS.md and ARCHITECTURE.md for the
build/release/architecture detail, bootstrap/README.md for the
two-level deployment install pattern, and zddc/README.md for the
HTTP server.
2026-04-27 11:05:47 -05:00

225 lines
8.1 KiB
JavaScript

(function (app) {
'use strict';
const dom = app.dom;
// Convert existing state to reactive state
if (app.createReactiveState) {
const oldState = app.state;
app.state = app.createReactiveState({
mode: oldState.mode || 'edit',
published: oldState.published || false,
dirty: oldState.dirty || false
});
// Subscribe to state changes to automatically update UI
app.state.subscribe(function(property, newValue, oldValue) {
// Auto-apply state changes
if (property === 'mode' || property === 'published') {
state.updateHiddenFields();
state.apply();
// Sync preview checkbox to current mode
if (app.modules.files && app.modules.files.syncPreviewCheckbox) {
app.modules.files.syncPreviewCheckbox();
}
}
if (property === 'dirty') {
// Could update UI indicator here
}
});
}
const state = app.state;
state.updateHiddenFields = function updateHiddenFields() {
const modeInput = dom.qs('#mode');
const publishedInput = dom.qs('#published');
if (modeInput) {
modeInput.value = state.mode;
}
if (publishedInput) {
publishedInput.value = state.published ? 'true' : 'false';
}
};
function toggleEditOnlyElements() {
const isEdit = state.mode === 'edit';
dom.qsa('[data-edit-only]').forEach(function (element) {
const value = element.getAttribute('data-edit-only');
if (value === 'true') {
dom.show(element, isEdit);
} else if (value === 'false') {
dom.show(element, !isEdit);
}
});
}
function updateDirectoryDependentControls() {
const isEdit = state.mode === 'edit';
const hasDirectory = !!app.data.selectedDirHandle;
dom.qsa('[data-requires-directory="true"]').forEach(function (element) {
const shouldDisable = !(isEdit && hasDirectory);
if ('disabled' in element) {
element.disabled = shouldDisable;
} else {
if (shouldDisable) {
element.setAttribute('aria-disabled', 'true');
} else {
element.removeAttribute('aria-disabled');
}
}
});
}
function updateResponseDueVisibility() {
var typeDisplay = dom.qs('#type-display');
var typeHidden = dom.qs('#type');
var wrapper = dom.qs('#response-due-wrapper');
if (!wrapper) { return; }
var typeVal = '';
if (typeDisplay) {
typeVal = (typeDisplay.textContent || '').trim();
} else if (typeHidden) {
typeVal = (typeHidden.value || '').trim();
}
var isSubmittal = typeVal.toLowerCase() === 'submittal';
if (isSubmittal) {
wrapper.classList.remove('hidden');
} else {
wrapper.classList.add('hidden');
}
}
state.apply = function applyState() {
updateResponseDueVisibility();
const inputs = document.querySelectorAll('input, select, textarea');
inputs.forEach(function (element) {
const noDisable = element.hasAttribute('data-no-disable') || (!!element.closest('thead') && !!element.closest('.filter-row'));
element.disabled = (state.mode !== 'edit') && !noDisable;
});
['#owner-name', '#project-name', '#project-number', '#type-display'].forEach(function (selector) {
const element = dom.qs(selector);
if (element) {
element.contentEditable = (state.mode === 'edit') ? 'true' : 'false';
}
});
const remarksTextarea = dom.qs('#remarks');
const remarksContainer = dom.qs('#remarks-render-container');
var mdEditor = app.modules.markdownEditor;
const isEdit = (state.mode === 'edit') && !state.published;
if (remarksTextarea) { remarksTextarea.hidden = true; }
if (mdEditor) {
if (isEdit) {
// Show rendered preview with click-to-edit; editor loads on first click
mdEditor.bindRenderClick();
mdEditor.setRenderClickable(true);
mdEditor.showRendered();
} else {
// View mode: destroy editor, show static rendered HTML
mdEditor.destroy();
mdEditor.setRenderClickable(false);
mdEditor.refreshRender();
if (remarksContainer) { remarksContainer.hidden = false; }
}
} else if (remarksContainer) {
remarksContainer.hidden = false;
}
const titleInput = dom.qs('#title');
if (titleInput) {
const empty = !(titleInput.value || '').trim();
titleInput.hidden = (state.mode === 'view' && empty);
}
// From field: input in edit mode, rendered mailto link in view mode
const fromInput = dom.qs('#from');
const fromRender = dom.qs('#from-render');
if (fromInput && fromRender) {
if (isEdit) {
fromInput.hidden = false;
fromRender.classList.add('hidden');
fromRender.hidden = true;
} else {
fromInput.hidden = true;
fromRender.classList.remove('hidden');
fromRender.hidden = false;
if (app.modules.emailTags && app.modules.emailTags.renderFrom) {
app.modules.emailTags.renderFrom();
}
}
}
// To field: input in edit mode, rendered mailto links in view mode
const toInput = dom.qs('#to');
const toRender = dom.qs('#to-render');
if (toInput && toRender) {
if (isEdit) {
toInput.hidden = false;
toRender.classList.add('hidden');
toRender.hidden = true;
} else {
toInput.hidden = true;
toRender.classList.remove('hidden');
toRender.hidden = false;
if (app.modules.emailTags && app.modules.emailTags.render) {
app.modules.emailTags.render();
}
}
}
// Logo placeholders: show in edit mode when no logo loaded
document.querySelectorAll('.logo-cell').forEach(function (cell) {
var img = cell.querySelector('.logo-img');
var placeholder = cell.querySelector('.logo-placeholder');
if (!placeholder) { return; }
var hasLogo = img && img.getAttribute('src');
if (hasLogo) {
cell.classList.add('has-logo');
} else {
cell.classList.remove('has-logo');
}
if (state.mode === 'edit' && !hasLogo) {
placeholder.classList.remove('hidden');
} else {
placeholder.classList.add('hidden');
}
});
toggleEditOnlyElements();
updateDirectoryDependentControls();
};
state.detectState = function detectState() {
var data = app.json.parse();
var envelope = (data && data.envelope) || {};
var payload = (data && data.payload) || {};
var presentation = (data && data.presentation) || {};
if (envelope.digest) { return 'published'; }
var hasDate = !!(payload.date && payload.date.trim());
if (hasDate) { return 'draft'; }
var hasHeader = !!(payload.client || payload.project || payload.projectNumber ||
presentation.leftLogo || presentation.rightLogo);
if (hasHeader) { return 'template'; }
return 'clean';
};
app.registerInit(function () {
state.updateHiddenFields();
toggleEditOnlyElements();
updateDirectoryDependentControls();
var typeInput = dom.qs('#type');
if (typeInput) {
typeInput.addEventListener('input', updateResponseDueVisibility);
}
updateResponseDueVisibility();
});
})(window.transmittalApp);