PageSourceSearch

https://smrtbld.ai/js/utils/download.js

js smrtbld.ai collected 2026-10-05 15:10:37 UTC 5,803 bytes, 144 lines download raw bytes

1/**
2 * Download utilities — kills the boilerplate around Blob → file download.
3 *
4 * The pattern below was duplicated across 31+ files (44+ call sites). Each site
5 * was hand-rolling `URL.createObjectURL + <a> element + appendChild + click +
6 * removeChild + URL.revokeObjectURL`. This module consolidates that into a
7 * single function so callers can focus on what they're downloading, not the
8 * mechanics of triggering it.
9 *
10 * @see PATTERNS_BIBLE.md — "Never roll your own when a util exists"
11 */
12
13/**
14 * Trigger a browser download from a Blob.
15 *
16 * Replaces the boilerplate:
17 *   const url = URL.createObjectURL(blob);
18 *   const a = document.createElement('a');
19 *   a.href = url; a.download = fileName;
20 *   document.body.appendChild(a); a.click(); document.body.removeChild(a);
21 *   URL.revokeObjectURL(url);
22 *
23 * Optionally fires a success toast — saves a follow-up `window.showToast?.()`
24 * call at the caller, and ensures success messaging stays consistent across
25 * exporters.
26 *
27 * @param {Blob} blob - The blob to download (e.g. PDF / XLSX / DOCX). Required.
28 * @param {string} [fileName='download'] - Download filename with extension.
29 *                                          Falls back to 'download' if missing.
30 * @param {Object} [options]
31 * @param {string} [options.successMessage] - Toast title to show after the
32 *                                             download is triggered. Omit for silent.
33 * @param {string} [options.successDescription] - Optional toast subtitle.
34 *                                                 Usually the filename or extra context.
35 * @returns {void}
36 *
37 * @example
38 *   // Simplest — silent download
39 *   downloadBlob(blob, 'Cash_Flow.pdf');
40 *
41 * @example
42 *   // With success toast
43 *   downloadBlob(blob, fileName, {
44 *       successMessage: 'Cash Flow PDF downloaded',
45 *   });
46 *
47 * @example
48 *   // Inside a try/catch around an export API call
49 *   try {
50 *       const { blob, fileName } = await api.exportPdf(projectId);
51 *       downloadBlob(blob, fileName || 'Export.pdf', { successMessage: 'PDF downloaded' });
52 *   } catch (err) {
53 *       if (err.name === 'AbortError') return;
54 *       window.showToast?.('Export failed', 'error', { description: err.message });
55 *   }
56 */
57export function downloadBlob(blob, fileName = 'download', options = {}) {
58    if (!blob) return;
59
60    const url = URL.createObjectURL(blob);
61    const a = document.createElement('a');
62    a.href = url;
63    a.download = fileName || 'download';
64    a.style.display = 'none';
65    document.body.appendChild(a);
66    a.click();
67    document.body.removeChild(a);
68    URL.revokeObjectURL(url);
69
70    if (options.successMessage) {
71        const toastOpts = options.successDescription
72            ? { description: options.successDescription }
73            : undefined;
74        window.showToast?.(options.successMessage, 'success', toastOpts);
75    }
76}
77
78/**
79 * Trigger a browser download for a server URL (no blob in hand) — e.g. the
80 * FILE_READY notification auto-download, where the backend generated the file
81 * and the notification carries its link. For in-memory blobs use downloadBlob.
82 *
83 * @param {string} url - Absolute or app-relative file URL. Required.
84 * @param {string} [fileName] - Optional download-attribute filename hint.
85 */
86export function downloadUrl(url, fileName = '') {
87    if (!url) return;
88
89    const a = document.createElement('a');
90    a.href = url;
91    a.download = fileName;
92    a.style.display = 'none';
93    document.body.appendChild(a);
94    a.click();
95    document.body.removeChild(a);
96}
97
98/**
99 * Open a Blob in a new browser tab for inline preview (e.g. a PDF the user
100 * wants to look at without saving). The created object URL is revoked after
101 * a delay so the new tab has time to finish loading before the URL invalidates.
102 *
103 * Replaces the boilerplate:
104 *   const url = URL.createObjectURL(blob);
105 *   window.open(url, '_blank');
106 *   setTimeout(() => URL.revokeObjectURL(url), 60_000);
107 *
108 * @param {Blob} blob - The blob to preview (typically a PDF). Required.
109 * @param {Object} [options]
110 * @param {number} [options.revokeAfterMs=60000] - Delay before revoking the object URL.
111 *                                                  60s is a safe default for a PDF
112 *                                                  to load in the new tab.
113 * @param {string} [options.popupBlockedMessage='Popup blocked'] - Toast title shown when
114 *                                                                  the browser blocks
115 *                                                                  `window.open`. Pass an
116 *                                                                  empty string to silence.
117 * @returns {boolean} `true` if the window was opened (or attempted), `false` if the
118 *                     blob was missing or the browser blocked the popup.
119 *
120 * @example
121 *   // Inside a Send modal's "preview" link
122 *   await safeAsync(async () => {
123 *       const { blob } = await api.fetchPdfBlob(projectId, id);
124 *       previewBlob(blob);
125 *   }, { label: 'AccInvoices', toast: 'Preview failed' });
126 */
127export function previewBlob(blob, options = {}) {
128    if (!blob) return false;
129    const url = URL.createObjectURL(blob);
130    const win = window.open(url, '_blank');
131    // No cross-tab "PDF loaded" event we can observe, so the timeout is the
132    // pragmatic cleanup — 60s is enough for a PDF to render in the new tab.
133    setTimeout(() => URL.revokeObjectURL(url), options.revokeAfterMs ?? 60_000);
134    if (!win) {
135        const title = options.popupBlockedMessage ?? 'Popup blocked';
136        if (title) {
137            window.showToast?.(title, 'error', {
138                description: 'Allow popups for this site and try again.',
139            });
140        }
141        return false;
142    }
143    return true;
144}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.