mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
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:
+91
-44
@@ -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 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;
|
||||||
|
|
||||||
const dx = event.clientX - this.#dragState.startX;
|
// Apply scroll (browser will automatically clamp to valid range)
|
||||||
const dy = event.clientY - this.#dragState.startY;
|
|
||||||
|
|
||||||
console.log("[FixedLayout Debug] 🎯 Dragging:", {
|
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user