ZDDC/classifier/js/excel.js
ZDDC 8ba029612e feat(shared): non-blocking toast helper available to every tool
Promote classifier's local toast (classifier/css/base.css + showToast
in classifier/js/excel.js) into shared/toast.{js,css}. Every tool's
build.sh now concatenates them, so window.zddc.toast(msg, level, opts)
is callable from any tool.

API:
  window.zddc.toast('Saved.', 'success');
  window.zddc.toast('Could not load: ' + err.message, 'error');
  window.zddc.toast('Note', 'info', { durationMs: 3000 });

Levels: info (default) | success | warning | error. Single-toast
policy — a second call replaces the first. Click anywhere on the
toast to dismiss. ARIA: error → role=alert/aria-live=assertive,
others → role=status/aria-live=polite.

Class prefix is .zddc-toast (BEM-ish) to avoid colliding with any
tool-local .toast rules. Classifier's existing showToast now
delegates to window.zddc.toast — call sites in excel.js +
selection.js are unchanged. Classifier's local .toast CSS block
deleted in favor of the shared one.

This commit only EXPOSES the API. Replacing the ~25 alert() call
sites scattered across archive/transmittal/mdedit/classifier with
toast calls is left as follow-up — each alert needs per-call review
to decide if it's truly non-blocking.

Five Playwright tests in tests/toast.spec.js lock the contract:
API exposure, level mapping, ARIA roles, single-toast replace,
click-to-dismiss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 19:04:41 -05:00

127 lines
4.2 KiB
JavaScript

/**
* Excel Integration Module
* Toast notifications and hash export
*/
(function() {
'use strict';
/**
* Thin wrapper over the shared toast helper. Keeps the
* window.app.modules.excel.showToast call sites in classifier
* unchanged while delegating to the canonical implementation in
* shared/toast.js (window.zddc.toast).
*/
function showToast(message, type = 'info') {
if (window.zddc && typeof window.zddc.toast === 'function') {
window.zddc.toast(message, type);
} else {
// shared/toast.js missing from the build — log so the
// problem is visible without crashing the caller.
console.warn('[classifier] window.zddc.toast unavailable;', type, message);
}
}
/**
* Export SHA256 hashes in sha256sum format
*/
async function exportHashes() {
const files = window.app.modules.store.getDisplayFiles();
if (files.length === 0) {
alert('No files to export');
return;
}
// Check if SHA256 is enabled
if (!window.app.calculateSha256) {
alert('Please enable SHA256 checkbox first and wait for hashes to calculate');
return;
}
try {
// Build sha256sum format: hash *filepath
const lines = [];
// Get root path
const rootPath = await getFullPath(window.app.rootHandle);
for (const file of files) {
if (!file.sha256 || file.sha256 === 'calculating...' || file.sha256 === 'error') {
continue; // Skip files without calculated hash
}
// Get full path from root
const folderPath = await getFullPath(file.folderHandle);
const fullPath = `${folderPath}/${zddc.joinExtension(file.originalFilename, file.extension)}`;
// Format: hash *filepath (asterisk indicates binary mode)
lines.push(`${file.sha256} *${fullPath}`);
}
if (lines.length === 0) {
alert('No SHA256 hashes available. Enable SHA256 and wait for calculation to complete.');
return;
}
// Create output
const output = lines.join('\n');
// Generate filename with timestamp
const now = new Date();
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, -5); // YYYY-MM-DDTHH-MM-SS
const filename = `sha256sums_${timestamp}.txt`;
// Download as file
const blob = new Blob([output], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Show success message
showToast(`✓ Downloaded ${lines.length} hash(es) to ${filename}`, 'success');
} catch (err) {
console.error('Error exporting hashes:', err);
alert('Error exporting hashes: ' + err.message);
}
}
/**
* Get full path from directory handle (all the way to root)
*/
async function getFullPath(dirHandle) {
const parts = [];
let current = dirHandle;
// Walk up to root - collect ALL parent folders
while (current) {
parts.unshift(current.name);
try {
// Try to get parent
if (typeof current.getParent === 'function') {
const parent = await current.getParent();
if (parent && parent !== current) {
current = parent;
continue;
}
}
break;
} catch {
break;
}
}
return parts.join('/');
}
// Export module
window.app.modules.excel = {
showToast,
exportHashes
};
})();