| Name | Message | Date |
|---|---|---|
| 📄 dialog.ts | 10 hours ago | |
| 📄 loadingTab.ts | 10 hours ago | |
| 📄 strings.ts | 10 hours ago |
📄
src/ui/loadingTab.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import { LOADING_SUBTITLE, LOADING_TITLE, formatMegabytes } from './strings';
/**
* A helper tab that acts as the loading indicator: opened synchronously on
* click (so it is never popup-blocked), shows a spinner + progress bar while
* the PDF streams, and is then navigated to the finished PDF blob.
*/
export interface LoadingTab {
/** Navigate the helper tab to the finished PDF. */
openPdf(blobUrl: string): void;
/** Progress callback, wired into the PDF download. */
setProgress(loadedBytes: number, totalBytes: number | null): void;
/** Did the user close the helper tab? */
isClosed(): boolean;
close(): void;
}
const LOADING_HTML = `<!doctype html>
<html lang="sv">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${LOADING_TITLE}</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; margin: 0; }
body {
font-family: 'Lato', -apple-system, 'Segoe UI', system-ui, sans-serif;
background: #f4f4f5;
color: #2a2d31;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.card {
background: #fff;
border-radius: 14px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12);
padding: 40px 48px 32px;
width: min(420px, calc(100vw - 48px));
text-align: center;
}
.spinner {
width: 44px;
height: 44px;
margin: 0 auto 20px;
border-radius: 50%;
border: 4px solid #f0d5d6;
border-top-color: #bd2428;
animation: vfpe-spin 0.9s linear infinite;
}
@keyframes vfpe-spin { to { transform: rotate(360deg); } }
h1 { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
.sub { font-size: 14px; color: #6b6f76; margin-bottom: 22px; line-height: 1.5; }
.track { height: 8px; border-radius: 999px; background: #ececed; overflow: hidden; }
.bar {
height: 100%;
width: 0%;
border-radius: 999px;
background: #bd2428;
transition: width 0.2s ease;
}
.bar.indeterminate { width: 35%; animation: vfpe-slide 1.1s ease-in-out infinite; }
@keyframes vfpe-slide {
0% { margin-left: -35%; }
100% { margin-left: 100%; }
}
.status { font-size: 13px; color: #6b6f76; margin-top: 12px; min-height: 1em; }
footer { margin-top: 26px; font-size: 12px; color: #a3a6ac; }
</style>
</head>
<body>
<main class="card">
<div class="spinner" role="status" aria-label="Laddar"></div>
<h1>${LOADING_TITLE}</h1>
<p class="sub">${LOADING_SUBTITLE}</p>
<div class="track"><div class="bar indeterminate" id="vfpe-bar"></div></div>
<p class="status" id="vfpe-status">Hämtar…</p>
<footer>VF PDF-opener</footer>
</main>
</body>
</html>`;
/** Open the loading tab. Returns null if the browser blocked the popup. */
export function openLoadingTab(): LoadingTab | null {
let win: Window | null = null;
try {
win = window.open('', '_blank');
} catch {
return null;
}
if (!win) return null;
try {
const doc = win.document;
doc.open();
doc.write(LOADING_HTML);
doc.close();
} catch {
try {
win.close();
} catch {
// Ignore — the tab will just sit there empty until closed manually.
}
return null;
}
const bar = win.document.getElementById('vfpe-bar') as HTMLDivElement | null;
const status = win.document.getElementById('vfpe-status') as HTMLParagraphElement | null;
return {
openPdf(blobUrl: string) {
try {
win!.location.replace(blobUrl);
} catch {
// The tab was closed between the check and the navigation — ignore.
}
},
setProgress(loadedBytes: number, totalBytes: number | null) {
if (!bar || !status) return;
if (totalBytes !== null && totalBytes > 0) {
bar.classList.remove('indeterminate');
const percent = Math.min(100, Math.round((loadedBytes / totalBytes) * 100));
bar.style.width = `${percent}%`;
status.textContent = `${formatMegabytes(loadedBytes)} av ${formatMegabytes(totalBytes)}`;
} else {
status.textContent = `${formatMegabytes(loadedBytes)} hämtat…`;
}
},
isClosed() {
return win!.closed;
},
close() {
if (!win!.closed) win!.close();
},
};
}