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.
437 lines
15 KiB
JavaScript
437 lines
15 KiB
JavaScript
(function() {
|
||
'use strict';
|
||
// Party presets for archive browser — IIFE module
|
||
|
||
// State (module scope, NOT on window.app)
|
||
let presets = [];
|
||
let activePresetName = null;
|
||
let isOpen = false;
|
||
let isNamingMode = false;
|
||
|
||
// Get localStorage key based on source mode and directory
|
||
function getStorageKey() {
|
||
if (window.app.sourceMode === 'http' && window.app.directories.length > 0) {
|
||
var u = window.app.directories[0].url || '';
|
||
return 'zddc-presets:http:' + u;
|
||
} else if (window.app.sourceMode === 'local' && window.app.directories.length > 0) {
|
||
return 'zddc-presets:local:' + window.app.directories[0].name;
|
||
}
|
||
return 'zddc-presets:default';
|
||
}
|
||
|
||
// Load presets from localStorage
|
||
function loadFromStorage() {
|
||
try {
|
||
var stored = localStorage.getItem(getStorageKey());
|
||
if (stored) {
|
||
var parsed = JSON.parse(stored);
|
||
if (parsed && Array.isArray(parsed.presets)) {
|
||
presets = parsed.presets;
|
||
} else {
|
||
presets = [];
|
||
}
|
||
} else {
|
||
presets = [];
|
||
}
|
||
} catch (e) {
|
||
presets = [];
|
||
}
|
||
}
|
||
|
||
// Save presets to localStorage
|
||
function saveToStorage() {
|
||
try {
|
||
localStorage.setItem(getStorageKey(), JSON.stringify({ presets: presets }));
|
||
} catch (e) {
|
||
// Silently fail on storage errors
|
||
}
|
||
}
|
||
|
||
// Load a preset by name
|
||
function loadPreset(name) {
|
||
var preset = presets.find(p => p.name === name);
|
||
if (!preset) return;
|
||
|
||
// Filter paths to only include folders that exist in groupingFolders
|
||
var validPaths = preset.paths.filter(p =>
|
||
window.app.groupingFolders.some(f => f.path === p)
|
||
);
|
||
|
||
window.app.selectedGroupingFolders = new Set(validPaths);
|
||
window.app.selectAllGroupingFolders = false;
|
||
|
||
var checkbox = document.getElementById('selectAllGroupingCheckbox');
|
||
if (checkbox) checkbox.checked = false;
|
||
|
||
activePresetName = name;
|
||
|
||
// Trigger UI updates
|
||
window.app.modules.app.updateFolderSelectionState('groupingFoldersList');
|
||
window.app.modules.app.renderTransmittalFolders();
|
||
window.app.modules.filtering.applyFilters();
|
||
|
||
renderButton();
|
||
renderDropdown();
|
||
}
|
||
|
||
// Save current selection as a preset
|
||
function savePreset(name) {
|
||
// Build paths array from current selection
|
||
var paths = Array.from(window.app.selectedGroupingFolders);
|
||
|
||
// Upsert preset
|
||
var existingIndex = presets.findIndex(p => p.name === name);
|
||
if (existingIndex >= 0) {
|
||
presets[existingIndex] = { name: name, paths: paths };
|
||
} else {
|
||
presets.push({ name: name, paths: paths });
|
||
}
|
||
|
||
saveToStorage();
|
||
activePresetName = name;
|
||
isNamingMode = false;
|
||
|
||
renderButton();
|
||
renderDropdown();
|
||
}
|
||
|
||
// Delete a preset by name
|
||
function deletePreset(name) {
|
||
presets = presets.filter(p => p.name !== name);
|
||
if (activePresetName === name) {
|
||
activePresetName = null;
|
||
}
|
||
saveToStorage();
|
||
renderButton();
|
||
renderDropdown();
|
||
}
|
||
|
||
// Check if current selection differs from active preset
|
||
function checkDirty() {
|
||
if (activePresetName === null) return;
|
||
|
||
var preset = presets.find(p => p.name === activePresetName);
|
||
if (!preset) return;
|
||
|
||
var currentPaths = new Set(window.app.selectedGroupingFolders);
|
||
var presetPaths = new Set(preset.paths || []);
|
||
|
||
// Compare sets
|
||
var dirty = currentPaths.size !== presetPaths.size ||
|
||
!Array.from(currentPaths).every(p => presetPaths.has(p));
|
||
|
||
if (dirty) {
|
||
renderButton();
|
||
}
|
||
}
|
||
|
||
// Get minimum depth of grouping folders (for top-level Only)
|
||
function getMinDepth() {
|
||
if (window.app.groupingFolders.length === 0) return 1;
|
||
return Math.min.apply(null, window.app.groupingFolders.map(f => f.path.split('/').length));
|
||
}
|
||
|
||
// Render the preset button label
|
||
function renderButton() {
|
||
var btn = document.getElementById('presetBtn');
|
||
if (!btn) return;
|
||
|
||
if (activePresetName !== null) {
|
||
// Check if dirty
|
||
var preset = presets.find(p => p.name === activePresetName);
|
||
var dirty = false;
|
||
if (preset) {
|
||
var currentPaths = new Set(window.app.selectedGroupingFolders);
|
||
var presetPaths = new Set(preset.paths || []);
|
||
dirty = currentPaths.size !== presetPaths.size ||
|
||
!Array.from(currentPaths).every(p => presetPaths.has(p));
|
||
}
|
||
btn.textContent = '▾ ' + activePresetName + (dirty ? '*' : '');
|
||
} else {
|
||
btn.textContent = '▾ Presets';
|
||
}
|
||
}
|
||
|
||
// Escape HTML for safe insertion
|
||
function escapeHtml(text) {
|
||
var div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Render the dropdown panel
|
||
function renderDropdown() {
|
||
var dropdown = document.getElementById('presetDropdown');
|
||
if (!dropdown) return;
|
||
|
||
var minDepth = getMinDepth();
|
||
|
||
// Build presets list HTML
|
||
var presetsHtml = '';
|
||
if (presets.length === 0) {
|
||
presetsHtml = '<div class="preset-no-presets"><i>No saved presets</i></div>';
|
||
} else {
|
||
presetsHtml = presets.map(preset => {
|
||
var escapedName = escapeHtml(preset.name);
|
||
return (
|
||
'<div class="preset-item" data-name="' + escapedName + '">' +
|
||
'<span>' + escapedName + '</span>' +
|
||
'<button class="preset-delete" data-name="' + escapedName + '">×</button>' +
|
||
'</div>'
|
||
);
|
||
}).join('');
|
||
}
|
||
|
||
// Build project checkboxes HTML
|
||
var projectsHtml = '';
|
||
window.app.groupingFolders.forEach(folder => {
|
||
// Only include top-level folders (minDepth)
|
||
var pathParts = folder.path.split('/');
|
||
if (pathParts.length !== minDepth) return;
|
||
|
||
var isSelected = window.app.selectedGroupingFolders.has(folder.path);
|
||
var escapedPath = escapeHtml(folder.path);
|
||
var escapedName = escapeHtml(folder.name);
|
||
|
||
projectsHtml += (
|
||
'<div class="preset-project-item">' +
|
||
'<label class="preset-project-label">' +
|
||
'<input type="checkbox" class="preset-checkbox" data-path="' + escapedPath + '"' +
|
||
(isSelected ? ' checked' : '') + '>' +
|
||
' ' + escapedName +
|
||
'</label>' +
|
||
'</div>'
|
||
);
|
||
});
|
||
|
||
// Footer HTML
|
||
var footerHtml = '';
|
||
if (activePresetName !== null) {
|
||
// Check if dirty
|
||
var preset = presets.find(p => p.name === activePresetName);
|
||
var dirty = false;
|
||
if (preset) {
|
||
var currentPaths = new Set(window.app.selectedGroupingFolders);
|
||
var presetPaths = new Set(preset.paths || []);
|
||
dirty = currentPaths.size !== presetPaths.size ||
|
||
!Array.from(currentPaths).every(p => presetPaths.has(p));
|
||
}
|
||
|
||
if (isNamingMode) {
|
||
footerHtml = (
|
||
'<div class="preset-footer-naming">' +
|
||
'<input type="text" class="preset-name-input" placeholder="Preset name" autoFocus>' +
|
||
'<button class="preset-confirm-name btn btn-sm">✓</button>' +
|
||
'<button class="preset-cancel-name btn btn-sm">✗</button>' +
|
||
'</div>'
|
||
);
|
||
} else if (dirty) {
|
||
footerHtml = (
|
||
'<div class="preset-footer-actions">' +
|
||
'<button class="preset-update-btn btn btn-primary btn-sm">Update "' + escapeHtml(activePresetName) + '"</button>' +
|
||
'<button class="preset-save-new-btn btn btn-secondary btn-sm">Save as New</button>' +
|
||
'</div>'
|
||
);
|
||
} else {
|
||
footerHtml = (
|
||
'<div class="preset-footer-actions">' +
|
||
'<button class="preset-save-new-btn btn btn-primary btn-sm">Save as New</button>' +
|
||
'</div>'
|
||
);
|
||
}
|
||
} else {
|
||
// No active preset — disabled if nothing selected
|
||
var selectedCount = window.app.selectedGroupingFolders.size;
|
||
var disabledAttr = selectedCount === 0 ? ' disabled' : '';
|
||
footerHtml = (
|
||
'<div class="preset-footer-actions">' +
|
||
'<button class="preset-save-btn btn btn-primary btn-sm' + (selectedCount === 0 ? ' btn-disabled' : '') + '" ' +
|
||
'data-disabled="' + (selectedCount === 0 ? 'true' : 'false') + '">Save as Preset</button>' +
|
||
'</div>'
|
||
);
|
||
}
|
||
|
||
dropdown.innerHTML = (
|
||
'<div class="preset-section-top">' +
|
||
'<div class="preset-section-label">Saved Presets:</div>' +
|
||
'<div class="preset-list">' + presetsHtml + '</div>' +
|
||
'</div>' +
|
||
'<div class="preset-divider"></div>' +
|
||
'<div class="preset-section-bottom">' +
|
||
'<div class="preset-section-label">Projects:</div>' +
|
||
'<div class="preset-projects-list">' + projectsHtml + '</div>' +
|
||
'</div>' +
|
||
footerHtml
|
||
);
|
||
}
|
||
|
||
// Toggle dropdown visibility
|
||
function toggleDropdown() {
|
||
var dropdown = document.getElementById('presetDropdown');
|
||
if (isOpen) {
|
||
closeDropdown();
|
||
} else {
|
||
isOpen = true;
|
||
if (dropdown) dropdown.classList.remove('hidden');
|
||
renderDropdown();
|
||
}
|
||
}
|
||
|
||
// Close dropdown
|
||
function closeDropdown() {
|
||
isOpen = false;
|
||
var dropdown = document.getElementById('presetDropdown');
|
||
if (dropdown) dropdown.classList.add('hidden');
|
||
isNamingMode = false;
|
||
}
|
||
|
||
// Set up event delegation on dropdown
|
||
function setupDropdownDelegation() {
|
||
var dropdown = document.getElementById('presetDropdown');
|
||
if (!dropdown) return;
|
||
|
||
dropdown.addEventListener('click', function(e) {
|
||
// Close on clicks inside dropdown
|
||
e.stopPropagation();
|
||
|
||
// Preset item click — load preset (do NOT close dropdown)
|
||
var presetItem = e.target.closest('.preset-item');
|
||
if (presetItem && !e.target.classList.contains('preset-delete')) {
|
||
var name = presetItem.getAttribute('data-name');
|
||
if (name) loadPreset(name);
|
||
return;
|
||
}
|
||
|
||
// Delete button
|
||
var deleteBtn = e.target.closest('.preset-delete');
|
||
if (deleteBtn) {
|
||
e.stopPropagation();
|
||
var name = deleteBtn.getAttribute('data-name');
|
||
if (name) deletePreset(name);
|
||
return;
|
||
}
|
||
|
||
// Checkbox click
|
||
var checkbox = e.target.closest('.preset-checkbox');
|
||
if (checkbox) {
|
||
var path = checkbox.getAttribute('data-path');
|
||
if (path) {
|
||
if (checkbox.checked) {
|
||
window.app.selectedGroupingFolders.add(path);
|
||
} else {
|
||
window.app.selectedGroupingFolders.delete(path);
|
||
}
|
||
window.app.modules.app.updateFolderSelectionState('groupingFoldersList');
|
||
window.app.modules.app.renderTransmittalFolders();
|
||
window.app.modules.filtering.applyFilters();
|
||
checkDirty();
|
||
renderButton();
|
||
renderDropdown(); // Re-render to update checkbox states and footer
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Save button (not in naming mode)
|
||
var saveBtn = e.target.closest('.preset-save-btn');
|
||
if (saveBtn && !isNamingMode) {
|
||
if (saveBtn.getAttribute('data-disabled') !== 'true') {
|
||
isNamingMode = true;
|
||
renderDropdown();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Update button — save current selection as active preset
|
||
var updateBtn = e.target.closest('.preset-update-btn');
|
||
if (updateBtn) {
|
||
if (activePresetName) savePreset(activePresetName);
|
||
return;
|
||
}
|
||
|
||
// Save as New button
|
||
var saveNewBtn = e.target.closest('.preset-save-new-btn');
|
||
if (saveNewBtn) {
|
||
isNamingMode = true;
|
||
renderDropdown();
|
||
return;
|
||
}
|
||
|
||
// Confirm name input
|
||
var confirmBtn = e.target.closest('.preset-confirm-name');
|
||
if (confirmBtn) {
|
||
var input = dropdown.querySelector('.preset-name-input');
|
||
if (input && input.value.trim()) {
|
||
savePreset(input.value.trim());
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Cancel name input
|
||
var cancelBtn = e.target.closest('.preset-cancel-name');
|
||
if (cancelBtn) {
|
||
isNamingMode = false;
|
||
renderDropdown();
|
||
return;
|
||
}
|
||
});
|
||
|
||
// Keydown on name input
|
||
dropdown.addEventListener('keydown', function(e) {
|
||
var input = e.target.closest('.preset-name-input');
|
||
if (!input) return;
|
||
|
||
if (e.key === 'Enter') {
|
||
e.stopPropagation();
|
||
if (input.value.trim()) {
|
||
savePreset(input.value.trim());
|
||
}
|
||
} else if (e.key === 'Escape') {
|
||
e.stopPropagation();
|
||
isNamingMode = false;
|
||
renderDropdown();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Handle outside click to close dropdown
|
||
function setupOutsideClickHandler() {
|
||
document.addEventListener('click', function(e) {
|
||
var section = document.getElementById('presetSection');
|
||
var dropdown = document.getElementById('presetDropdown');
|
||
if (isOpen && section && dropdown && !section.contains(e.target)) {
|
||
closeDropdown();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Initialize presets module — called after first scan completes
|
||
function init() {
|
||
// Idempotent: skip if button listener already attached
|
||
var btn = document.getElementById('presetBtn');
|
||
if (!btn || btn.dataset.presetInit) return;
|
||
btn.dataset.presetInit = '1';
|
||
|
||
btn.addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
toggleDropdown();
|
||
});
|
||
|
||
setupDropdownDelegation();
|
||
setupOutsideClickHandler();
|
||
loadFromStorage();
|
||
renderButton();
|
||
}
|
||
|
||
// Register module
|
||
window.app.modules.presets = {
|
||
init: init,
|
||
loadPreset: loadPreset,
|
||
savePreset: savePreset,
|
||
deletePreset: deletePreset,
|
||
checkDirty: checkDirty,
|
||
renderButton: renderButton,
|
||
toggleDropdown: toggleDropdown,
|
||
closeDropdown: closeDropdown
|
||
};
|
||
|
||
})();
|