Fix right iframe zoom and improve pan/drag behavior in fixed-layout e-books

This commit addresses several issues with zoom and pan functionality,
particularly when zooming from the right iframe in a two-page spread:

- Add source iframe tracking to wheel and mouse events
  * Pass frameId parameter through frame creation pipeline
  * Tag events with sourceIframe and sourceFrame properties
  * Enable proper cursor position calculation regardless of event source

- Fix zoom cursor calculation for two-page spreads
  * Calculate cursor position relative to spread viewport, not individual pages
  * Simplify scroll position calculation to work uniformly for both left and right pages
  * Add debug logging for cursor calculation verification

- Improve drag/pan behavior across zoom modes
  * Add draggable state property to control drag availability
  * Handle fit-page mode differently with dragOffset tracking
  * Direct scroll manipulation for zoom levels above 1
  * Add console logging for ignored drag scenarios

- Prevent native image drag interference
  * Set draggable="false" on all images in iframe documents
  * Disable user select and webkit drag on images
  * Prevent default behavior on iframe mouse events

These changes ensure that zoom operations work correctly from both left and
right iframes, maintaining proper cursor focus and scroll position.
This commit is contained in:
2026-04-17 16:56:18 -04:00
parent 5b822b682b
commit 60332911e6
+89 -42
View File
@@ -59,6 +59,7 @@ export class FixedLayout extends HTMLElement {
startY: 0, startY: 0,
scrollLeft: 0, scrollLeft: 0,
scrollTop: 0, scrollTop: 0,
draggable: true,
}; };
dragOffset = { x: 0, y: 0 }; dragOffset = { x: 0, y: 0 };
#magnifier = { #magnifier = {
@@ -235,12 +236,31 @@ export class FixedLayout extends HTMLElement {
const delta = event.deltaY; const delta = event.deltaY;
const rect = this.getBoundingClientRect(); const rect = this.getBoundingClientRect();
// DETERMINE WHICH IFRAME SENT THE EVENT
const source = event.sourceIframe || "right"; // Default to right
const leftWidth = this.#left?.element?.offsetWidth || 0;
// ADJUST CURSOR POSITION BASED ON SOURCE IFRAME
const rawCursorX = event.clientX - rect.left;
const rawCursorY = event.clientY - rect.top;
// For both pages, calculate cursor position relative to the spread's viewport
// The cursor position is already relative to the container, not individual pages
const cursorX = rawCursorX;
const cursorY = rawCursorY;
// Current zoom level (default to fit-page if not set) // Current zoom level (default to fit-page if not set)
const oldScale = const oldScale =
this.#zoom === "fit-page" || this.#zoom === "fit-width" this.#zoom === "fit-page" || this.#zoom === "fit-width"
? 1 ? 1
: this.#zoom || 1; : this.#zoom || 1;
console.log("[FixedLayout Debug] 📍 Cursor calculation:", {
source,
leftWidth,
rawCursorX,
cursorX,
rectLeft: rect.left,
clientX: event.clientX,
});
// Calculate new scale // Calculate new scale
const zoomFactor = const zoomFactor =
delta > 0 ? 1 - this.#zoomState.zoomStep : 1 + this.#zoomState.zoomStep; delta > 0 ? 1 - this.#zoomState.zoomStep : 1 + this.#zoomState.zoomStep;
@@ -249,11 +269,9 @@ export class FixedLayout extends HTMLElement {
Math.max(this.#zoomState.minScale, oldScale * zoomFactor), Math.max(this.#zoomState.minScale, oldScale * zoomFactor),
); );
// Zoom toward cursor position
const cursorX = event.clientX - rect.left;
const cursorY = event.clientY - rect.top;
const scaleChange = newScale / oldScale; const scaleChange = newScale / oldScale;
// Calculate new scroll position (same calculation for both pages)
const newScrollX = const newScrollX =
(this.scrollLeft + this.dragOffset.x + cursorX) * scaleChange - (this.scrollLeft + this.dragOffset.x + cursorX) * scaleChange -
cursorX - cursorX -
@@ -308,12 +326,10 @@ export class FixedLayout extends HTMLElement {
if (this.#dragState.isPotentialDrag) { if (this.#dragState.isPotentialDrag) {
console.log("[FixedLayout Debug] ✋ DRAG MODE ACTIVATED (timeout)"); console.log("[FixedLayout Debug] ✋ DRAG MODE ACTIVATED (timeout)");
this.#dragState.isDragging = true; this.#dragState.isDragging = true;
this.#dragState.isPotentialDrag = false;
this.dragOffset.x = 0; this.dragOffset.x = 0;
this.dragOffset.y = 0; this.dragOffset.y = 0;
this.style.cursor = "grabbing"; this.style.cursor = "grabbing";
// Prevent text selection while dragging
event.preventDefault();
} }
}, this.#dragState.pressDelay); }, this.#dragState.pressDelay);
} }
@@ -325,18 +341,8 @@ export class FixedLayout extends HTMLElement {
const dy = event.clientY - this.#dragState.startY; const dy = event.clientY - this.#dragState.startY;
const moved = Math.sqrt(dx * dx + dy * dy); const moved = Math.sqrt(dx * dx + dy * dy);
console.log("[FixedLayout Debug] 📐 Measuring movement:", {
dx,
dy,
moved: moved.toFixed(2),
threshold: 3,
});
// If moved more than 3 pixels, start dragging immediately // If moved more than 3 pixels, start dragging immediately
if (moved > 3) { if (moved > 3) {
console.log(
"[FixedLayout Debug] ✋ DRAG MODE ACTIVATED (movement > 3px)",
);
clearTimeout(this.#dragState.dragTimeout); clearTimeout(this.#dragState.dragTimeout);
this.#dragState.isPotentialDrag = false; this.#dragState.isPotentialDrag = false;
this.#dragState.isDragging = true; this.#dragState.isDragging = true;
@@ -347,28 +353,40 @@ export class FixedLayout extends HTMLElement {
} }
// Handle actual drag // Handle actual drag
if (this.#dragState.isDragging) { if (!this.#dragState.isDragging) return;
event.preventDefault();
const dx = event.clientX - this.#dragState.startX; const dx = event.clientX - this.#dragState.startX;
const dy = event.clientY - this.#dragState.startY; const dy = event.clientY - this.#dragState.startY;
if (this.#zoom === "fit-page") {
// Track how far user has dragged
this.dragOffset.x -= dx;
this.dragOffset.y -= dy;
this.#render();
return;
}
if (
this.#zoom !== "fit-page" &&
this.#zoom !== "fit-width" &&
this.#zoom !== 1 &&
this.#dragState.draggable
) {
// Calculate new scroll position directly from mouse movement
const newScrollX = this.#dragState.scrollLeft - dx;
const newScrollY = this.#dragState.scrollTop - dy;
console.log("[FixedLayout Debug] 🎯 Dragging:", { // Apply scroll (browser will automatically clamp to valid range)
dx,
dy,
from: `(${this.dragOffset.x}, ${this.dragOffset.y})`,
to: `(${this.dragOffset.x - dx}, ${this.dragOffset.y - dy})`,
});
// Update drag offset instead of scrolling
this.dragOffset.x += dx;
this.dragOffset.y += dy;
// Re-render with new drag offset
const newScrollX = this.#dragState.scrollLeft - this.dragOffset.x;
const newScrollY = this.#dragState.scrollTop - this.dragOffset.y;
this.scrollLeft = newScrollX; this.scrollLeft = newScrollX;
this.scrollTop = newScrollY; this.scrollTop = newScrollY;
} else {
const reason = !this.#dragState.draggable
? "draggable is false"
: this.#zoom === "fit-page"
? "zoom is fit-page"
: this.#zoom === "fit-width"
? "zoom is fit-width"
: this.#zoom === 1
? "zoom is 1"
: "unknown";
console.log(`[FixedLayout Debug] ⛔ Drag ignored - ${reason}`);
} }
} }
@@ -377,6 +395,7 @@ export class FixedLayout extends HTMLElement {
wasDragging: this.#dragState.isDragging, wasDragging: this.#dragState.isDragging,
wasPotentialDrag: this.#dragState.isPotentialDrag, wasPotentialDrag: this.#dragState.isPotentialDrag,
hasTimeout: !!this.#dragState.dragTimeout, hasTimeout: !!this.#dragState.dragTimeout,
isDraggingBeforeReset: this.#dragState.isDragging, // ADD THIS
}); });
// Clear any pending drag timeout // Clear any pending drag timeout
if (this.#dragState.dragTimeout) { if (this.#dragState.dragTimeout) {
@@ -400,7 +419,7 @@ export class FixedLayout extends HTMLElement {
} }
} }
async #createFrame({ index, src: srcOption }) { async #createFrame({ index, src: srcOption }, frameId) {
const srcOptionIsString = typeof srcOption === "string"; const srcOptionIsString = typeof srcOption === "string";
const src = srcOptionIsString ? srcOption : srcOption?.src; const src = srcOptionIsString ? srcOption : srcOption?.src;
const onZoom = srcOptionIsString ? null : srcOption?.onZoom; const onZoom = srcOptionIsString ? null : srcOption?.onZoom;
@@ -419,7 +438,7 @@ export class FixedLayout extends HTMLElement {
iframe.setAttribute("scrolling", "no"); iframe.setAttribute("scrolling", "no");
iframe.setAttribute("part", "filter"); iframe.setAttribute("part", "filter");
this.#root.append(element); this.#root.append(element);
if (!src) return { blank: true, element, iframe }; if (!src) return { blank: true, element, iframe, frameId };
return new Promise((resolve) => { return new Promise((resolve) => {
iframe.addEventListener( iframe.addEventListener(
"load", "load",
@@ -431,7 +450,7 @@ export class FixedLayout extends HTMLElement {
const { width, height } = getViewport(doc, this.defaultViewport); const { width, height } = getViewport(doc, this.defaultViewport);
// NEW: Attach event listeners INSIDE the iframe document // NEW: Attach event listeners INSIDE the iframe document
this.#attachEventListenersToIframe(doc); this.#attachEventListenersToIframe(doc, frameId, { element, iframe });
resolve({ resolve({
element, element,
@@ -439,6 +458,7 @@ export class FixedLayout extends HTMLElement {
width: parseFloat(width), width: parseFloat(width),
height: parseFloat(height), height: parseFloat(height),
onZoom, onZoom,
frameId,
}); });
}, },
{ once: true }, { once: true },
@@ -446,7 +466,7 @@ export class FixedLayout extends HTMLElement {
iframe.src = src; iframe.src = src;
}); });
} }
#attachEventListenersToIframe(doc) { #attachEventListenersToIframe(doc, frameId, frame) {
if (!doc) { if (!doc) {
console.log("[FixedLayout Debug] ⏸️ No document to attach listeners"); console.log("[FixedLayout Debug] ⏸️ No document to attach listeners");
return; return;
@@ -456,6 +476,17 @@ export class FixedLayout extends HTMLElement {
"[FixedLayout Debug] 🔧 Attaching event listeners to iframe document", "[FixedLayout Debug] 🔧 Attaching event listeners to iframe document",
); );
const images = doc.querySelectorAll("img");
images.forEach((img) => {
img.setAttribute("draggable", "false");
img.style.userSelect = "none";
img.style.webkitUserDrag = "none";
img.style.WebkitUserDrag = "none";
});
console.log(
`[FixedLayout Debug] 🔒 Disabled native drag on ${images.length} images`,
);
// Attach wheel event listener for zoom to the iframe's document // Attach wheel event listener for zoom to the iframe's document
doc.addEventListener( doc.addEventListener(
"wheel", "wheel",
@@ -476,6 +507,10 @@ export class FixedLayout extends HTMLElement {
cancelable: true, cancelable: true,
}); });
// TAG EVENT WITH SOURCE IFRAME
fixedLayoutEvent.sourceIframe = frameId;
fixedLayoutEvent.sourceFrame = frame;
// Call our handler with the event // Call our handler with the event
this.#handleWheel(fixedLayoutEvent); this.#handleWheel(fixedLayoutEvent);
@@ -512,7 +547,11 @@ export class FixedLayout extends HTMLElement {
cancelable: true, cancelable: true,
}); });
// TAG EVENT WITH SOURCE IFRAME
mouseEvent.sourceIframe = frameId;
mouseEvent.sourceFrame = frame;
this.#handleMouseDown(mouseEvent); this.#handleMouseDown(mouseEvent);
event.preventDefault();
}); });
doc.addEventListener("mousemove", (event) => { doc.addEventListener("mousemove", (event) => {
@@ -528,7 +567,11 @@ export class FixedLayout extends HTMLElement {
cancelable: true, cancelable: true,
}); });
// TAG EVENT WITH SOURCE IFRAME
mouseEvent.sourceIframe = frameId;
mouseEvent.sourceFrame = frame;
this.#handleMouseMove(mouseEvent); this.#handleMouseMove(mouseEvent);
event.preventDefault();
}); });
doc.addEventListener("mouseup", (event) => { doc.addEventListener("mouseup", (event) => {
@@ -544,7 +587,11 @@ export class FixedLayout extends HTMLElement {
cancelable: true, cancelable: true,
}); });
// TAG EVENT WITH SOURCE IFRAME
mouseEvent.sourceIframe = frameId;
mouseEvent.sourceFrame = frame;
this.#handleMouseUp(mouseEvent); this.#handleMouseUp(mouseEvent);
event.preventDefault();
}); });
console.log( console.log(
@@ -624,12 +671,12 @@ export class FixedLayout extends HTMLElement {
this.#right = null; this.#right = null;
this.#center = null; this.#center = null;
if (center) { if (center) {
this.#center = await this.#createFrame(center); this.#center = await this.#createFrame(center, "center");
this.#side = "center"; this.#side = "center";
this.#render(); this.#render();
} else { } else {
this.#left = await this.#createFrame(left); this.#left = await this.#createFrame(left, "left");
this.#right = await this.#createFrame(right); this.#right = await this.#createFrame(right, "right");
this.#side = this.#left.blank this.#side = this.#left.blank
? "right" ? "right"
: this.#right.blank : this.#right.blank