Files
JetCore/jetc/barant-site/assets/images/documents.js
T
Brad Arant c1491b044b Add IMAP webmail tag, admin/opensource subpage tiers, store/cart, htmleditor, uploads
New __imap*/__smtpsend/__eval Jet tags give templates real IMAP
(list/message/flag/delete/move/search/folders/accounts) and SMTP-send
capability, backing the new /email webmail client (email.jet/
email.menu, assets/images/email.js + newemail.js) and its
mainpagetemplate.jet-based page shell. Most of this tag family's
source was already being built into the live site (Tag.cpp already
registered __imap/__imaplist/__imapmessage/__imapflag/__imapdelete/
__eval) but had never actually been committed; this also registers
and commits the remaining five (__imapmove/__imapfolders/
__imapsearch/__smtpsend/__imapaccounts).

admin.page/admin.menu gain a real subpage tier (admin-email,
admin-newemail, admin-invoices, admin-products, admin-users) replacing
several previously top-level pages, mirrored by a similar
opensource.page subpage tier (opensource-overview/asm1802/barantmail/
jet/serverframework/synthmax) - old top-level pages retired via git
rename (asm1802.page, jet.page, serverframework.page,
admin_invoices.page); barantmail.page is dropped outright, folded into
opensource-barantmail.subpage.

New storefront (store.page, store-createstore.subpage,
barantstore.page) and cart flow (cart.jet, updatecartqty.proc,
removecartitem.proc, editproduct.page/selectproduct.proc/
update_product.proc for inventory maintenance against the barant
database).

New document upload/download manager (documents.jet, documents_api.jet,
documents_upload.proc, documents_delete.proc,
assets/images/documents.js + documents-page.js) with a persistent
floating panel, and a rich-text page editor (htmleditor.jet/.page,
createhtmleditor.proc, savehtmleditor.proc, assets/images/htmleditor.js
+ its icon/logo assets).

SESSIONCTL.tag: login now redirects back to the originally-requested
page instead of always landing on /account ($[start:0] vs $[start]
variable-name mismatch).

main.css/menu.bar/invoices.menu/jet_overview.section/login.menu:
styling and navigation updates for the above; new site assets (fonts,
images, audio) referenced by them.

jetc/build-assets.sh, list_jet_files.sh, lowercase_jet.py,
strip_fragments.py: small dev tooling used while building the above.
2026-09-06 04:14:00 +00:00

179 lines
6.3 KiB
JavaScript

// Site-wide file transfer manager - loaded once via mainpagetemplate.jet
// (see its own script tag, outside #content) so the floating panel and
// any in-progress transfer survive spa.js's own #content-only swaps
// when navigating to a different page mid-upload/download.
(function() {
var panel = document.getElementById('filemgr-panel');
if (!panel)
return;
var toggle = document.getElementById('filemgr-toggle');
var badge = document.getElementById('filemgr-badge');
var list = document.getElementById('filemgr-list');
var clearBtn = document.getElementById('filemgr-clear');
var transfers = {};
var nextId = 1;
function escapeHtml(s) {
var div = document.createElement('div');
div.textContent = s == null ? '' : String(s);
return div.innerHTML;
}
function updateBadge() {
var activeCount = 0;
for (var k in transfers) {
if (transfers[k].status === 'active')
activeCount++;
}
badge.textContent = activeCount > 0 ? String(activeCount) : '';
}
function removeEmptyPlaceholder() {
var empty = document.getElementById('filemgr-empty');
if (empty)
empty.remove();
}
function showEmptyPlaceholderIfNeeded() {
if (Object.keys(transfers).length === 0 && !document.getElementById('filemgr-empty'))
list.innerHTML = '<div id="filemgr-empty">No transfers yet.</div>';
}
var UPLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V6M5 12l7-7 7 7"/></svg>';
var DOWNLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v13M5 12l7 7 7-7"/></svg>';
function addTransfer(name, direction) {
var id = 'xfer' + (nextId++);
removeEmptyPlaceholder();
var el = document.createElement('div');
el.className = 'filemgr-item';
el.innerHTML =
'<div class="filemgr-item-row">' +
'<span class="filemgr-item-icon">' + (direction === 'upload' ? UPLOAD_ICON : DOWNLOAD_ICON) + '</span>' +
'<span class="filemgr-item-name">' + escapeHtml(name) + '</span>' +
'<span class="filemgr-item-status">0%</span>' +
'</div>' +
'<div class="filemgr-progress-track"><div class="filemgr-progress-fill"></div></div>';
list.insertBefore(el, list.firstChild);
transfers[id] = { el: el, status: 'active' };
panel.classList.add('filemgr-expanded');
updateBadge();
return id;
}
function updateProgress(id, percent) {
var t = transfers[id];
if (!t)
return;
t.el.querySelector('.filemgr-progress-fill').style.width = percent + '%';
t.el.querySelector('.filemgr-item-status').textContent = percent + '%';
}
function markDone(id, statusText) {
var t = transfers[id];
if (!t)
return;
t.status = 'done';
t.el.classList.add('filemgr-done');
t.el.querySelector('.filemgr-progress-fill').style.width = '100%';
t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Done';
updateBadge();
}
function markFailed(id, statusText) {
var t = transfers[id];
if (!t)
return;
t.status = 'failed';
t.el.classList.add('filemgr-failed');
t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Failed';
updateBadge();
}
// Uploads a FileList/array of File objects, each as its own
// concurrent XHR request - the browser's own HTTP connection pooling
// runs several at once, tracked here as separate panel entries with
// independent progress via xhr.upload's own progress event (fetch()
// has no equivalent for the request/upload side, only the response).
window.fileManagerUpload = function(files) {
for (var i = 0; i < files.length; i++) {
(function(file) {
var id = addTransfer(file.name, 'upload');
var xhr = new XMLHttpRequest();
transfers[id].xhr = xhr;
xhr.open('POST', '/documents/api/upload');
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable)
updateProgress(id, Math.round((e.loaded / e.total) * 100));
});
xhr.addEventListener('load', function() {
var res = null;
try { res = JSON.parse(xhr.responseText); } catch (e) {}
if (xhr.status === 200 && res && res.ok) {
markDone(id);
document.dispatchEvent(new CustomEvent('documents:changed'));
} else {
markFailed(id, (res && res.error) || 'Failed');
}
});
xhr.addEventListener('error', function() { markFailed(id); });
var fd = new FormData();
fd.append('name', file.name);
fd.append('filedata', file);
xhr.send(fd);
})(files[i]);
}
};
// Downloads a document by id via XHR (responseType 'blob') instead
// of a plain <a href> so it gets real tracked progress in the same
// panel as uploads, then hands the fetched bytes to the browser's
// normal save flow via a Blob URL + synthetic <a download> click.
window.fileManagerDownload = function(id, name) {
var xferId = addTransfer(name, 'download');
var xhr = new XMLHttpRequest();
transfers[xferId].xhr = xhr;
xhr.open('GET', '/documents/download?id=' + encodeURIComponent(id));
xhr.responseType = 'blob';
xhr.addEventListener('progress', function(e) {
if (e.lengthComputable)
updateProgress(xferId, Math.round((e.loaded / e.total) * 100));
});
xhr.addEventListener('load', function() {
if (xhr.status !== 200) {
markFailed(xferId);
return;
}
var blobUrl = URL.createObjectURL(xhr.response);
var a = document.createElement('a');
a.href = blobUrl;
a.download = name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function() { URL.revokeObjectURL(blobUrl); }, 30000);
markDone(xferId);
});
xhr.addEventListener('error', function() { markFailed(xferId); });
xhr.send();
};
toggle.addEventListener('click', function() {
panel.classList.toggle('filemgr-expanded');
});
clearBtn.addEventListener('click', function() {
for (var k in transfers) {
if (transfers[k].status !== 'active') {
transfers[k].el.remove();
delete transfers[k];
}
}
showEmptyPlaceholderIfNeeded();
});
})();