Skip to content

Commit cf84423

Browse files
wikirbyclaude
andcommitted
Allow mode switching during initial FullPage capture
V3 previously locked all non-FullPage mode buttons until the screenshot loop completed. V1 allowed switching during capture; restoring that UX. Renderer: - Drop the three mode-button disable loops (initial lock, switchToFullPage re-entry, sign-out reset). Save + signout remain locked during capture. - New abortCurrentCapture() centralizes the cancel teardown: sends cancelCapture to worker, clears local progress UI, restores the preview-frame overlay, resets stitchYOffset so the next session starts clean. - Click handler invokes abortCurrentCapture() when leaving FullPage mid-capture (replacing the previous early-return that blocked the switch entirely). - switchToFullPage's re-entry path defers kickoffFullPageCapture through a two-step requestAnimationFrame, with an explicit iframe.contentWindow.scrollTo(0, 0) after a forced reflow. Two paint frames is enough for the iframe to commit the display:none -> display:block transition AND the scroll reset before the worker's captureVisibleTab fires. Without this, captureVisibleTab grabs the iframe at its prior scroll position and the new capture appears to "continue where it left off". - Finalize's UI cleanup guards on currentMode === "fullpage" so a capture that completes while the user has already pivoted to another mode doesn't force-enable the Save button or steal focus. Worker: - New cancelCapture action handler sets a captureCancelled flag scoped to setupPort. - Four async-resume points (the scrollResult setTimeout, the windows.update callback, the captureVisibleTab callback, the drawComplete handler) short-circuit on the flag. - The dimensions handler resets the flag to false on every fresh kickoff so the new session starts clean. Aria intact (1x microsoft.applications.telemetry.LogManager in minified target/chrome/logManager.js); npm run build:prod completes clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f342370 commit cf84423

2 files changed

Lines changed: 50 additions & 35 deletions

File tree

‎src/scripts/extensions/webExtensionBase/webExtensionWorker.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,8 @@ export class WebExtensionWorker extends ExtensionWorkerBase<W3CTab, number> {
311311
let captureCount = 0;
312312
let lastScrollY = -1;
313313
let lastScrollData: { scrollY: number; pageHeight: number };
314+
// Set true on cancelCapture to short-circuit the capture loop and stop focus-stealing.
315+
let captureCancelled = false;
314316

315317
// Per-port save accumulator — images streamed via saveImage chunks
316318
let pendingSave: any = undefined; // tslint:disable-line:no-null-keyword
@@ -428,6 +430,7 @@ export class WebExtensionWorker extends ExtensionWorkerBase<W3CTab, number> {
428430
captureCount = 0;
429431
lastScrollY = -1;
430432
lastScrollData = { scrollY: 0, pageHeight: 0 };
433+
captureCancelled = false;
431434

432435
if (!viewportHeight) {
433436
cleanup();
@@ -445,12 +448,19 @@ export class WebExtensionWorker extends ExtensionWorkerBase<W3CTab, number> {
445448
port.postMessage({ action: "scroll", scrollTo: 0 });
446449
}
447450

451+
if (message.action === "cancelCapture") {
452+
captureCancelled = true;
453+
}
454+
448455
if (message.action === "scrollResult") {
449456
lastScrollData = { scrollY: message.scrollY, pageHeight: message.pageHeight };
450457
setTimeout(() => {
451-
// Re-focus renderer window before capture — handles user clicking away
458+
if (captureCancelled) { return; }
459+
// Re-focus renderer window before capture — handles user clicking away
452460
WebExtension.browser.windows.update(renderWindowId, { focused: true }, () => {
461+
if (captureCancelled) { return; }
453462
WebExtension.browser.tabs.captureVisibleTab(renderWindowId, { format: "png" }, (dataUrl: string) => {
463+
if (captureCancelled) { return; }
454464
if (!dataUrl) {
455465
cleanup();
456466
return;
@@ -472,8 +482,9 @@ export class WebExtensionWorker extends ExtensionWorkerBase<W3CTab, number> {
472482
}
473483

474484
if (message.action === "drawComplete") {
475-
// Detect scroll stall: if scrollY didn't change, we've hit the
476-
// real bottom even if scrollHeight is inflated
485+
if (captureCancelled) { return; }
486+
// Detect scroll stall: if scrollY didn't change, we've hit the
487+
// real bottom even if scrollHeight is inflated
477488
let scrollStalled = captureCount > 0 && lastScrollData.scrollY === lastScrollY;
478489
lastScrollY = lastScrollData.scrollY;
479490
captureCount++;

‎src/scripts/renderer.ts‎

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -897,22 +897,15 @@ async function fetchFreshNotebooks() {
897897
logTelemetryEvent(getNotebooksEvent);
898898
}
899899
}
900-
// Lock all interactive controls during initial capture — prevents race conditions
901-
// (e.g., clicking sign-out mid-capture corrupts state)
902-
document.querySelectorAll(".mode-btn").forEach((btn) => {
903-
if (btn.getAttribute("data-mode") !== "fullpage") {
904-
(btn as HTMLButtonElement).disabled = true;
905-
btn.classList.add("disabled");
906-
}
907-
});
900+
// Sign-out and Save lock until capture completes; mode buttons stay interactive.
908901
saveBtn.disabled = true;
909902
disableSignout();
910903
// Show initial capture progress (bar hidden until first drawCapture with viewport counts)
911904
capturePanel.style.display = "flex";
912905
statusText.textContent = strings.capturing;
913906
announceToScreenReader(strings.capturing);
914907
(document.getElementById("progress-bar-track") as HTMLElement).style.display = "none";
915-
// During capture, Cancel is the only actionable control — focus it
908+
// Cancel is the safest initial focus target; user can Tab to a mode button.
916909
if (isSignedIn) { setTimeout(function() { cancelBtn.focus(); }, 100); }
917910

918911
// Section selection persistence is handled by selectSection() in the custom dropdown
@@ -1013,17 +1006,18 @@ function switchToFullPage() {
10131006
statusText.textContent = strings.capturing;
10141007
saveBtn.disabled = true;
10151008
if (captureDimensions && !captureInProgress) {
1016-
document.querySelectorAll(".mode-btn").forEach((b: Element) => {
1017-
if (b.getAttribute("data-mode") !== "fullpage") {
1018-
(b as HTMLButtonElement).disabled = true;
1019-
b.classList.add("disabled");
1020-
}
1021-
});
10221009
disableSignout();
10231010
announceToScreenReader(strings.capturing);
10241011
let progressTrack = document.getElementById("progress-bar-track") as HTMLElement;
10251012
if (progressTrack) { progressTrack.style.display = "none"; }
1026-
kickoffFullPageCapture();
1013+
// Defer kickoff one paint so the iframe commits scrollTo(0,0) before captureVisibleTab fires.
1014+
void iframe.offsetHeight;
1015+
iframe.contentWindow?.scrollTo(0, 0);
1016+
requestAnimationFrame(() => {
1017+
requestAnimationFrame(() => {
1018+
kickoffFullPageCapture();
1019+
});
1020+
});
10271021
}
10281022
}
10291023
}
@@ -2389,8 +2383,8 @@ modeButtons.forEach((btn, idx) => {
23892383
btn.addEventListener("click", () => {
23902384
let mode = btn.getAttribute("data-mode");
23912385
if (mode === currentMode) { return; }
2392-
// Block mode switching mid-capture (captureVisibleTab needs the iframe visible).
2393-
if (captureInProgress && mode !== "fullpage") { return; }
2386+
// Pivoting away from fullpage mid-capture aborts; re-entry restarts.
2387+
if (captureInProgress && mode !== "fullpage") { abortCurrentCapture(); }
23942388

23952389
// Update selected state visually + ARIA
23962390
document.querySelectorAll(".mode-btn").forEach((b) => {
@@ -2436,7 +2430,6 @@ modeButtons.forEach((btn, idx) => {
24362430

24372431
// --- Full-page capture lifecycle helpers ---
24382432

2439-
// Sends `dimensions` to start the worker's scroll-and-stitch loop.
24402433
function kickoffFullPageCapture() {
24412434
if (!captureDimensions || captureInProgress || fullPageComplete) { return; }
24422435
captureInProgress = true;
@@ -2448,6 +2441,18 @@ function kickoffFullPageCapture() {
24482441
});
24492442
}
24502443

2444+
// Aborts the in-flight capture: tells the worker to stop the loop and clears the local progress UI.
2445+
function abortCurrentCapture() {
2446+
if (!captureInProgress) { return; }
2447+
safeSend({ action: "cancelCapture" });
2448+
captureInProgress = false;
2449+
stitchYOffset = 0;
2450+
capturePanel.style.display = "none";
2451+
let progressTrack = document.getElementById("progress-bar-track") as HTMLElement;
2452+
if (progressTrack) { progressTrack.style.display = "none"; }
2453+
showPreviewFrame();
2454+
}
2455+
24512456
// Post-loadContent state for invocations that don't need a full-page screenshot
24522457
// (Selection/Image context-menu or oEmbed-site toolbar invoke).
24532458
function enterReadyStateWithoutCapture() {
@@ -2935,22 +2940,23 @@ port.onMessage.addListener((message: any) => {
29352940
previewContainer.appendChild(previewImg);
29362941
}
29372942

2938-
// Update sidebar to preview mode — hide progress, show Clip button
2943+
// Hide capture progress in all modes — capture is done.
29392944
capturePanel.style.display = "none";
2940-
saveBtn.textContent = strings.saveToOneNote;
2941-
saveBtn.disabled = false;
2945+
// Active mode owns Save state; don't force-enable here if user pivoted.
2946+
if (currentMode === "fullpage") {
2947+
saveBtn.textContent = strings.saveToOneNote;
2948+
saveBtn.disabled = false;
2949+
}
29422950

2943-
// Re-enable mode buttons and sign-out now that capture is complete
2944-
document.querySelectorAll(".mode-btn").forEach((b: Element) => {
2945-
(b as HTMLButtonElement).disabled = false;
2946-
b.classList.remove("disabled");
2947-
});
29482951
enableSignout();
29492952
captureInProgress = false;
29502953
announceToScreenReader(loc("WebClipper.Label.ClipSuccessful", "Capture complete"));
29512954

2952-
let fpModeBtn = document.querySelector('.mode-btn[data-mode="fullpage"]') as HTMLElement;
2953-
if (fpModeBtn) { setTimeout(function() { fpModeBtn.focus(); }, 100); }
2955+
// Only re-focus fullpage button if user is still in fullpage.
2956+
if (currentMode === "fullpage") {
2957+
let fpModeBtn = document.querySelector('.mode-btn[data-mode="fullpage"]') as HTMLElement;
2958+
if (fpModeBtn) { setTimeout(function() { fpModeBtn.focus(); }, 100); }
2959+
}
29542960

29552961
safeSend({ action: "finalizeComplete" });
29562962
};
@@ -3262,11 +3268,9 @@ port.onMessage.addListener((message: any) => {
32623268
iframe.style.display = "none";
32633269
previewFrameWrap.style.display = "none";
32643270
capturePanel.style.display = "none";
3265-
// Reset mode buttons to initial state (disabled until capture completes)
3271+
// Buttons stay interactive; the sign-in overlay covers them while signed out.
32663272
document.querySelectorAll(".mode-btn").forEach((b) => {
32673273
b.classList.remove("selected");
3268-
(b as HTMLButtonElement).disabled = true;
3269-
b.classList.add("disabled");
32703274
});
32713275
let fpBtn = document.querySelector('.mode-btn[data-mode="fullpage"]');
32723276
if (fpBtn) { fpBtn.classList.add("selected"); }

0 commit comments

Comments
 (0)