// hovercard.js — rich-metadata tooltip for tree rows. // // Replaces the native title="…" attribute with a custom card that // surfaces every field we know about for a row: parsed ZDDC fields // (trackingNumber / revision / status / title / date), type, size, // modTime, on-server path, and URL. A delayed reveal (~350 ms) keeps // the card out of the way during fast traversal; it dismisses on // any click, right-click, scroll, or row change. // // Singleton DOM element appended to
; positioned fixed. (function () { 'use strict'; if (!window.app || !window.app.modules) return; var SHOW_DELAY_MS = 350; // Grace period after the cursor leaves the row before the card // hides. Lets the user move INTO the card to select / copy text; // the card cancels this timer on mouseenter. var HIDE_DELAY_MS = 200; var state = window.app.state; var card = null; var showTimer = null; var hideTimer = null; var currentRow = null; function ensureCard() { if (card) return card; card = document.createElement('div'); card.className = 'tree-hovercard'; card.setAttribute('aria-hidden', 'true'); // Mouse interaction inside the card: cancel any pending hide // so the user can stay in it as long as they want, then re- // schedule on leave. Pointer-events:auto in the CSS lets the // mouse enter; user-select:text (default) lets them drag a // selection; right-click inside fires the browser's native // Copy menu since we never call preventDefault for it here. card.addEventListener('mouseenter', cancelHide); card.addEventListener('mouseleave', scheduleHide); document.body.appendChild(card); return card; } function cancelHide() { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } } function scheduleHide() { cancelHide(); hideTimer = setTimeout(hide, HIDE_DELAY_MS); } function hide() { if (showTimer) { clearTimeout(showTimer); showTimer = null; } cancelHide(); if (card) card.classList.remove('is-visible'); currentRow = null; } // ── Formatting (kept local so this module is self-contained) ── function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function fmtSize(bytes) { if (bytes == null) return ''; if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; if (bytes < 1024 * 1024 * 1024) { return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } function fmtDate(d) { if (!d) return ''; var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); } function typeLabelFor(node) { if (node.isDir) return 'Folder'; if (node.isZip) return 'Zip archive'; if (node.ext) return node.ext.toUpperCase() + ' file'; return 'File'; } function buildRowsHtml(node) { var tree = window.app.modules.tree; var z = window.zddc; var parsed = null; if (z) { parsed = node.isDir ? z.parseFolder(node.name) : z.parseFilename(node.name); } var html = ''; // ZDDC fields first when the basename parses. if (parsed && parsed.valid) { if (parsed.date) html += kv('Date', parsed.date, true); if (parsed.trackingNumber) html += kv('Tracking number', parsed.trackingNumber, true); if (parsed.revision) html += kv('Revision', parsed.revision, true); if (parsed.status) html += kv('Status', parsed.status, true); if (parsed.title) html += kv('Title', parsed.title); html += ''; } else if (node.displayName) { // Operator-supplied display name — only useful as info if // it differs from the on-disk name. html += kv('Display name', node.displayName); } html += kv('Type', typeLabelFor(node)); if (!node.isDir) html += kv('Filename', node.name, true); if (!node.isDir && node.size != null) html += kv('Size', fmtSize(node.size)); if (node.modTime) html += kv('Modified', fmtDate(node.modTime)); if (node.virtual) html += kv('Virtual', 'Not yet created on disk'); // Path comes last (longest, most likely to wrap). var path = tree ? tree.pathFor(node) : ''; if (path) html += kv('Path', path, true); if (node.url && node.url !== path) html += kv('URL', node.url, true); return html; } function kv(key, val, mono) { return '' + escapeHtml(key) + '' + '' + escapeHtml(val) + ''; } function render(node) { var z = window.zddc; var parsed = z ? (node.isDir ? z.parseFolder(node.name) : z.parseFilename(node.name)) : null; var primary, secondary = ''; if (parsed && parsed.valid) { primary = parsed.title; var parts = node.isDir ? [parsed.date, parsed.trackingNumber, parsed.status] : [parsed.trackingNumber, parsed.revision, parsed.status]; secondary = parts.filter(Boolean).join(' · '); } else if (node.displayName) { primary = node.displayName; } else { primary = node.name; } card.innerHTML = '' + '