Fix panning bugs in FixedLayout zoom/pan mode

Several interrelated fixes for the drag/pan behavior when zoomed in:

- Add dedicated #onResize() handler: numeric zoom rescales frames in
  place (preserving pan position) while fit-page/fit-width does a full
  re-render. Replaces the old bare #render() in the ResizeObserver.

- Add resetZoom() public method that clears the numeric zoom state and
  re-renders, replacing the old removeAttribute('zoom') pattern which
  left stale internal state.

- Guard spread-side sync in #handleMouseUp with !this.#portrait so
  dual-frame logic only runs in landscape spreads, preventing incorrect
  side-switching in portrait mode.

- Guard #updateTransform spread offset with !portrait for the same
  reason — portrait layouts don't have left/right frames.

- Fix #handleMouseDown to return false when declining drag (select mode
  in PDFs) and return true when starting a drag, so the iframe event
  proxy can correctly decide whether to preventDefault.

- Only preventDefault on iframe mousemove when actively dragging or
  magnifier is enabled, allowing normal text selection to work.

- Store zoom level in #zoom during #zoomByRatio and dispatch zoom event
  instead of writing back to the DOM attribute.

- Add debug console.log statements to zoomByRatio and handleWheel for
  tracing transform calculations.
This commit is contained in:
2026-04-18 22:34:03 -04:00
parent 170352bc71
commit 7ec14df593
+53 -9
View File
@@ -25,7 +25,7 @@ const getViewport = (doc, viewport) => {
export class FixedLayout extends HTMLElement { export class FixedLayout extends HTMLElement {
static observedAttributes = ["zoom", "interaction-mode"]; static observedAttributes = ["zoom", "interaction-mode"];
#root = this.attachShadow({ mode: "closed" }); #root = this.attachShadow({ mode: "closed" });
#observer = new ResizeObserver(() => this.#render()); #observer = new ResizeObserver(() => this.#onResize());
#spreads; #spreads;
#index = -1; #index = -1;
defaultViewport; defaultViewport;
@@ -132,6 +132,11 @@ export class FixedLayout extends HTMLElement {
return this.#magnifier.enabled; return this.#magnifier.enabled;
} }
resetZoom() {
this.#zoom = undefined;
this.#render();
}
toggleMagnifier() { toggleMagnifier() {
this.#magnifier.enabled = !this.#magnifier.enabled; this.#magnifier.enabled = !this.#magnifier.enabled;
@@ -149,6 +154,17 @@ export class FixedLayout extends HTMLElement {
} }
} }
#onResize() {
if (typeof this.#zoom === "number" && !isNaN(this.#zoom)) {
// Numeric zoom: rescale frames but preserve user's pan position
this.#updateFrameScales(this.#transform.scale);
this.#applyTransform();
} else {
// Fit-page/fit-width: full re-render (recalculate scale + center)
this.#render();
}
}
#createMagnifier() { #createMagnifier() {
if (this.#magnifier.element) return; if (this.#magnifier.element) return;
@@ -379,6 +395,14 @@ export class FixedLayout extends HTMLElement {
} }
#zoomByRatio(cx, cy, ratio) { #zoomByRatio(cx, cy, ratio) {
console.log("[FXL] zoomByRatio:", {
cx,
cy,
ratio,
oldScale: this.#transform.scale,
oldX: this.#transform.x,
oldY: this.#transform.y,
});
const oldScale = this.#transform.scale; const oldScale = this.#transform.scale;
const newScale = Math.min( const newScale = Math.min(
this.#zoomState.maxScale, this.#zoomState.maxScale,
@@ -394,11 +418,15 @@ export class FixedLayout extends HTMLElement {
this.#updateFrameScales(newScale); this.#updateFrameScales(newScale);
this.#applyTransform(); this.#applyTransform();
this.setAttribute("zoom", newScale);
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("zoom", { detail: { scale: newScale } }), new CustomEvent("zoom", { detail: { scale: newScale } }),
); );
console.log("[FXL] zoomByRatio result:", {
newX: this.#transform.x,
newY: this.#transform.y,
newScale: this.#transform.scale,
actualRatio,
});
} }
#handleWheel(event) { #handleWheel(event) {
@@ -414,14 +442,23 @@ export class FixedLayout extends HTMLElement {
event.deltaY > 0 event.deltaY > 0
? 1 - this.#zoomState.zoomStep ? 1 - this.#zoomState.zoomStep
: 1 + this.#zoomState.zoomStep; : 1 + this.#zoomState.zoomStep;
console.log("[FXL] wheel:", {
cx,
cy,
transformX: this.#transform.x,
transformY: this.#transform.y,
transformScale: this.#transform.scale,
zoom: this.#zoom,
side: this.#side,
hostWidth: rect.width,
});
this.#zoomByRatio(cx, cy, ratio); this.#zoomByRatio(cx, cy, ratio);
} }
#handleMouseDown(event) { #handleMouseDown(event) {
if (event.button !== 0) return; if (event.button !== 0) return;
if (this.#isPDF && this.#interactionMode === "select" && !event.shiftKey) if (this.#isPDF && this.#interactionMode === "select" && !event.shiftKey)
return; return false;
this.#dragState.startX = event.clientX; this.#dragState.startX = event.clientX;
this.#dragState.startY = event.clientY; this.#dragState.startY = event.clientY;
@@ -429,6 +466,7 @@ export class FixedLayout extends HTMLElement {
this.#dragState.startTY = this.#transform.y; this.#dragState.startTY = this.#transform.y;
this.#dragState.isDragging = true; this.#dragState.isDragging = true;
this.style.cursor = "grabbing"; this.style.cursor = "grabbing";
return true;
} }
#handleMouseMove(event) { #handleMouseMove(event) {
@@ -448,7 +486,12 @@ export class FixedLayout extends HTMLElement {
this.style.cursor = ""; this.style.cursor = "";
// sync #side with the frame the user actually panned to // sync #side with the frame the user actually panned to
if (!this.#center && !this.#left?.blank && !this.#right?.blank) { if (
!this.#center &&
!this.#left?.blank &&
!this.#right?.blank &&
!this.#portrait
) {
const leftWidth = (this.#left.width ?? 0) * this.#transform.scale; const leftWidth = (this.#left.width ?? 0) * this.#transform.scale;
const viewportCenterInWrapper = const viewportCenterInWrapper =
this.getBoundingClientRect().width / 2 - this.#transform.x; this.getBoundingClientRect().width / 2 - this.#transform.x;
@@ -565,8 +608,8 @@ export class FixedLayout extends HTMLElement {
}); });
mouseEvent.sourceIframe = frameId; mouseEvent.sourceIframe = frameId;
mouseEvent.sourceFrame = frame; mouseEvent.sourceFrame = frame;
this.#handleMouseDown(mouseEvent); const dragStarted = this.#handleMouseDown(mouseEvent);
event.preventDefault(); if (dragStarted) event.preventDefault();
}); });
doc.addEventListener("mousemove", (event) => { doc.addEventListener("mousemove", (event) => {
@@ -588,6 +631,7 @@ export class FixedLayout extends HTMLElement {
mouseEvent.sourceIframe = frameId; mouseEvent.sourceIframe = frameId;
mouseEvent.sourceFrame = frame; mouseEvent.sourceFrame = frame;
this.#handleMouseMove(mouseEvent); this.#handleMouseMove(mouseEvent);
if (this.#dragState.isDragging || this.#magnifier.enabled)
event.preventDefault(); event.preventDefault();
}); });
@@ -664,7 +708,7 @@ export class FixedLayout extends HTMLElement {
const hasDualFrames = const hasDualFrames =
!this.#center && !this.#left?.blank && !this.#right?.blank; !this.#center && !this.#left?.blank && !this.#right?.blank;
if (isNumericZoom && hasDualFrames) { if (isNumericZoom && hasDualFrames && !portrait) {
const leftWidth = (this.#left.width ?? 0) * scale; const leftWidth = (this.#left.width ?? 0) * scale;
const rightWidth = (this.#right.width ?? 0) * scale; const rightWidth = (this.#right.width ?? 0) * scale;
if (side === "right") { if (side === "right") {