Skip to content

Commit 73cfd3f

Browse files
committed
add a progress bar for large files
1 parent 5a99f0c commit 73cfd3f

File tree

3 files changed

+128
-27
lines changed

3 files changed

+128
-27
lines changed

code.js

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
const errorText = document.getElementById('errorText');
6262
const toolbar = document.getElementById('toolbar');
6363
const statusBar = document.getElementById('statusBar');
64+
const progressBarOverlay = document.getElementById('progressBar');
65+
const progressBar = document.querySelector('#progressBar .progress');
6466
const originalStatus = document.getElementById('originalStatus');
6567
const generatedStatus = document.getElementById('generatedStatus');
6668

@@ -579,33 +581,64 @@
579581
const toolbarHeight = 32;
580582
const statusBarHeight = 32;
581583

582-
function finishLoading(code, map) {
584+
function waitForDOM() {
585+
return new Promise(r => setTimeout(r, 1));
586+
}
587+
588+
async function finishLoading(code, map) {
583589
const startTime = Date.now();
584590
promptText.style.display = 'none';
585591
toolbar.style.display = 'flex';
586592
statusBar.style.display = 'flex';
587593
canvas.style.display = 'block';
594+
originalStatus.textContent = generatedStatus.textContent = '';
595+
fileList.innerHTML = '';
596+
const option = document.createElement('option');
597+
option.textContent = `Loading...`;
598+
fileList.appendChild(option);
599+
fileList.disabled = true;
600+
fileList.selectedIndex = 0;
601+
originalTextArea = generatedTextArea = hover = null;
602+
isInvalid = true;
588603
updateHash(code, map);
604+
605+
// Let the browser update before parsing the source map, which may be slow
606+
await waitForDOM();
589607
const sm = parseSourceMap(map);
590608

591-
// Populate the file picker
592-
fileList.innerHTML = '';
593-
for (let sources = sm.sources, i = 0, n = sources.length; i < n; i++) {
594-
const option = document.createElement('option');
595-
option.textContent = `${i}: ${sources[i].name}`;
596-
fileList.appendChild(option);
597-
}
609+
// Show a progress bar if this is is going to take a while
610+
let charsSoFar = 0;
611+
let progressCalls = 0;
612+
let isProgressVisible = false;
613+
const progressStart = Date.now();
614+
const totalChars = code.length + (sm.sources.length > 0 ? sm.sources[0].content.length : 0);
615+
const progress = chars => {
616+
charsSoFar += chars;
617+
if (!isProgressVisible && progressCalls++ > 2 && charsSoFar) {
618+
const estimatedTimeLeftMS = (Date.now() - progressStart) / charsSoFar * (totalChars - charsSoFar);
619+
if (estimatedTimeLeftMS > 250) {
620+
progressBarOverlay.style.display = 'block';
621+
isProgressVisible = true;
622+
}
623+
}
624+
if (isProgressVisible) {
625+
progressBar.style.transform = `scaleX(${charsSoFar / totalChars})`;
626+
return waitForDOM();
627+
}
628+
};
629+
progressBar.style.transform = `scaleX(0)`;
598630

599631
// Update the original text area when the source changes
600632
const otherSource = index => index === -1 ? null : sm.sources[index].name;
601633
const originalName = index => sm.names[index];
602-
originalTextArea = null;
634+
let finalOriginalTextArea = null;
603635
if (sm.sources.length > 0) {
604-
const updateOriginalSource = () => {
605-
const source = sm.sources[fileList.selectedIndex];
606-
originalTextArea = createTextArea({
607-
sourceIndex: fileList.selectedIndex,
636+
const updateOriginalSource = (sourceIndex, progress) => {
637+
const source = sm.sources[sourceIndex];
638+
return createTextArea({
639+
sourceIndex,
608640
text: source.content,
641+
progress,
609642
mappings: source.data,
610643
mappingsOffset: 3,
611644
otherSource,
@@ -619,21 +652,18 @@
619652
};
620653
},
621654
});
655+
};
656+
fileList.onchange = async () => {
657+
originalTextArea = await updateOriginalSource(fileList.selectedIndex);
622658
isInvalid = true;
623659
};
624-
fileList.onchange = updateOriginalSource;
625-
updateOriginalSource();
626-
} else {
627-
const option = document.createElement('option');
628-
option.textContent = `(no original code)`;
629-
option.disabled = true;
630-
fileList.appendChild(option);
660+
finalOriginalTextArea = await updateOriginalSource(0, progress);
631661
}
632-
fileList.selectedIndex = 0;
633662

634-
generatedTextArea = createTextArea({
663+
generatedTextArea = await createTextArea({
635664
sourceIndex: null,
636665
text: code,
666+
progress,
637667
mappings: sm.data,
638668
mappingsOffset: 0,
639669
otherSource,
@@ -649,7 +679,27 @@
649679
},
650680
});
651681

682+
// Only render the original text area once the generated text area is ready
683+
originalTextArea = finalOriginalTextArea;
652684
isInvalid = true;
685+
686+
// Populate the file picker once there will be no more await points
687+
fileList.innerHTML = '';
688+
if (sm.sources.length > 0) {
689+
for (let sources = sm.sources, i = 0, n = sources.length; i < n; i++) {
690+
const option = document.createElement('option');
691+
option.textContent = `${i}: ${sources[i].name}`;
692+
fileList.appendChild(option);
693+
}
694+
fileList.disabled = false;
695+
} else {
696+
const option = document.createElement('option');
697+
option.textContent = `(no original code)`;
698+
fileList.appendChild(option);
699+
}
700+
fileList.selectedIndex = 0;
701+
702+
if (isProgressVisible) progressBarOverlay.style.display = 'none';
653703
const endTime = Date.now();
654704
console.log(`Finished loading in ${endTime - startTime}ms`);
655705
}
@@ -714,13 +764,15 @@
714764
let generatedTextArea;
715765
let hover = null;
716766

717-
function splitTextIntoLinesAndRuns(text) {
767+
async function splitTextIntoLinesAndRuns(text, progress) {
718768
c.font = monospaceFont;
719769
const spaceWidth = c.measureText(' ').width;
720770
const spacesPerTab = 2;
721771
const parts = text.split(/(\r\n|\r|\n)/g);
722772
const unicodeWidthCache = new Map();
723773
const lines = [];
774+
const progressChunkSize = 1 << 20;
775+
let prevProgressPoint = 0;
724776
let longestLineInColumns = 0;
725777
let lineStartOffset = 0;
726778

@@ -732,6 +784,7 @@
732784
continue;
733785
}
734786

787+
let nextProgressPoint = progress ? prevProgressPoint + progressChunkSize - lineStartOffset : Infinity;
735788
let runs = [];
736789
let i = 0;
737790
let n = raw.length + 1; // Add 1 for the extra character at the end
@@ -743,6 +796,13 @@
743796
let whitespace = 0;
744797
let isSingleChunk = false;
745798

799+
// Update the progress bar occasionally
800+
if (i > nextProgressPoint) {
801+
await progress(lineStartOffset + i - prevProgressPoint);
802+
prevProgressPoint = lineStartOffset + i;
803+
nextProgressPoint = i + progressChunkSize;
804+
}
805+
746806
while (i < n) {
747807
let c1 = raw.charCodeAt(i);
748808
let c2;
@@ -858,15 +918,19 @@
858918
lineStartOffset += raw.length;
859919
}
860920

921+
if (prevProgressPoint < text.length && progress) {
922+
await progress(text.length - prevProgressPoint);
923+
}
924+
861925
return { lines, longestLineInColumns };
862926
}
863927

864-
function createTextArea({ sourceIndex, text, mappings, mappingsOffset, otherSource, originalName, bounds }) {
928+
async function createTextArea({ sourceIndex, text, progress, mappings, mappingsOffset, otherSource, originalName, bounds }) {
865929
const shadowWidth = 16;
866930
const textPaddingX = 5;
867931
const textPaddingY = 1;
868932
const scrollbarThickness = 16;
869-
let { lines, longestLineInColumns } = splitTextIntoLinesAndRuns(text);
933+
let { lines, longestLineInColumns } = await splitTextIntoLinesAndRuns(text, progress);
870934
let animate = null;
871935
let lastLineIndex = lines.length - 1;
872936
let scrollX = 0;
@@ -1189,9 +1253,12 @@
11891253
} else {
11901254
if (originalTextArea.sourceIndex !== hover.mapping.originalSource) {
11911255
fileList.selectedIndex = hover.mapping.originalSource;
1192-
fileList.onchange();
1256+
fileList.onchange().then(() => {
1257+
originalTextArea.scrollTo(hover.mapping.originalColumn, hover.mapping.originalLine);
1258+
});
1259+
} else {
1260+
originalTextArea.scrollTo(hover.mapping.originalColumn, hover.mapping.originalLine);
11931261
}
1194-
originalTextArea.scrollTo(hover.mapping.originalColumn, hover.mapping.originalLine);
11951262
}
11961263
}
11971264
return;

index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ <h2>Generated&nbsp;code</h2>
5656
</path>
5757
</svg>
5858
</div>
59+
<div id="progressBar">
60+
<div class="progress"></div>
61+
</div>
5962
<script src="code.js"></script>
6063
</body>
6164

style.css

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,25 @@ noscript:before {
165165
margin-left: -50px;
166166
}
167167

168+
#progressBar {
169+
display: none;
170+
position: absolute;
171+
left: 50%;
172+
top: 50%;
173+
margin: -3px -100px;
174+
width: 200px;
175+
height: 6px;
176+
background: rgba(127, 127, 127, 0.3);
177+
z-index: 2;
178+
}
179+
180+
#progressBar .progress {
181+
width: 100%;
182+
height: 100%;
183+
transform-origin: left;
184+
will-change: transform;
185+
}
186+
168187
/* ---------- Light colors ---------- */
169188

170189
body:not([data-theme=dark]) {
@@ -178,6 +197,10 @@ body:not([data-theme=dark]) #theme-dark {
178197
display: none;
179198
}
180199

200+
body:not([data-theme=dark]) #progressBar .progress {
201+
background: #222;
202+
}
203+
181204
/* ---------- Dark colors ---------- */
182205

183206
body[data-theme=dark] {
@@ -195,6 +218,10 @@ body[data-theme=dark] #theme-dark {
195218
display: block;
196219
}
197220

221+
body[data-theme=dark] #progressBar .progress {
222+
background: #eee;
223+
}
224+
198225
/* ---------- Dark colors without JavaScript ---------- */
199226

200227
@media (prefers-color-scheme: dark) {
@@ -212,4 +239,8 @@ body[data-theme=dark] #theme-dark {
212239
body:not([data-theme=light]) #theme-dark {
213240
display: block;
214241
}
242+
243+
body:not([data-theme=light]) #progressBar .progress {
244+
background: #eee;
245+
}
215246
}

0 commit comments

Comments
 (0)