mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
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:
+53
-9
@@ -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") {
|
||||||
|
|||||||
Reference in New Issue
Block a user