Compare commits
1 commit
main
...
github-pag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d52773c5f |
4 changed files with 431 additions and 50 deletions
|
|
@ -32,7 +32,8 @@ import {
|
|||
swipListener,
|
||||
wheelListener,
|
||||
keyListener,
|
||||
selectListener
|
||||
selectListener,
|
||||
touchListener
|
||||
} from '../utils/listeners/listener'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -214,45 +215,54 @@ const registerEvents = () => {
|
|||
if (rendition) {
|
||||
rendition.on('rendered', (section, iframe) => {
|
||||
// Focus the iframe
|
||||
iframe?.iframe?.contentWindow.focus()
|
||||
iframe?.iframe?.contentWindow.focus();
|
||||
|
||||
// Register interaction listeners
|
||||
if (!epubOptions?.flow?.includes('scrolled')) {
|
||||
wheelListener(iframe.document, flipPage)
|
||||
wheelListener(iframe.document, flipPage);
|
||||
}
|
||||
swipListener(iframe.document, flipPage)
|
||||
keyListener(iframe.document, flipPage)
|
||||
|
||||
// Register your custom selection listener if toggleBubble is provided
|
||||
if (toggleBubble) {
|
||||
selectListener(iframe.document, rendition, toggleBubble)
|
||||
} else if (handleTextSelected) {
|
||||
// If no toggleBubble but handleTextSelected exists, use the built-in selection event
|
||||
rendition.on('selected', handleTextSelected)
|
||||
}
|
||||
// Use the unified touch handler instead of separate listeners
|
||||
touchListener(
|
||||
iframe.document,
|
||||
rendition,
|
||||
flipPage, // Navigation function
|
||||
toggleBubble || handleTextSelected // Selection function
|
||||
);
|
||||
|
||||
// Register regular click listener for non-touch interactions
|
||||
clickListener(iframe.document, rendition, flipPage);
|
||||
|
||||
// If no unified handler, fall back to separate listeners
|
||||
// swipListener(iframe.document, flipPage);
|
||||
// if (toggleBubble) {
|
||||
// selectListener(iframe.document, rendition, toggleBubble);
|
||||
// } else if (handleTextSelected) {
|
||||
// rendition.on('selected', handleTextSelected);
|
||||
// }
|
||||
|
||||
// Mark first content as displayed for location restoration
|
||||
if (!loadingState.firstContentDisplayed) {
|
||||
loadingState.firstContentDisplayed = true
|
||||
loadingState.firstContentDisplayed = true;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Location change tracking
|
||||
rendition.on('locationChanged', onLocationChange)
|
||||
// Other event handlers remain the same
|
||||
rendition.on('locationChanged', onLocationChange);
|
||||
|
||||
rendition.on('relocated', (location: any) => {
|
||||
// console.log('Book relocated to:', location)
|
||||
})
|
||||
rendition.on('relocated', (location) => {
|
||||
console.log('Book relocated to:', location);
|
||||
});
|
||||
|
||||
rendition.on('displayError', (err: any) => {
|
||||
console.error('Display error:', err)
|
||||
})
|
||||
rendition.on('displayError', (err) => {
|
||||
console.error('Display error:', err);
|
||||
});
|
||||
|
||||
if (handleKeyPress) {
|
||||
rendition.on('keypress', handleKeyPress)
|
||||
}
|
||||
rendition.on('keypress', handleKeyPress);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Function to apply saved location
|
||||
const applyPendingLocation = () => {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import keyListener from './key'
|
|||
import wheelListener from './wheel'
|
||||
import swipListener from './swip'
|
||||
import selectListener from './select'
|
||||
import touchListener from './touch'
|
||||
export {
|
||||
clickListener,
|
||||
keyListener,
|
||||
wheelListener,
|
||||
swipListener,
|
||||
selectListener,
|
||||
touchListener
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,58 @@
|
|||
// src/utils/listeners/selectListener.js
|
||||
|
||||
/**
|
||||
* Enhanced selection listener with touch support
|
||||
* @param {Document} document - The document object to add event
|
||||
* @param {Object} rendition - The EPUBJS rendition
|
||||
* @param {Function} fb - The listener function
|
||||
* @param {Function} fn - The listener function
|
||||
*/
|
||||
export default function selectListener(document, rendition, fn) {
|
||||
document.addEventListener('mousedown', () => {
|
||||
document.getSelection().removeAllRanges()
|
||||
fn('cleared')
|
||||
})
|
||||
// Track touch selection state
|
||||
let touchStarted = false;
|
||||
let touchSelection = false;
|
||||
let touchStartTime = 0;
|
||||
const LONG_PRESS_DURATION = 500; // ms
|
||||
const TOUCH_MOVE_THRESHOLD = 10; // pixels
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
|
||||
document.addEventListener('mouseup', (e) => {
|
||||
if (e.ignore) return
|
||||
e.ignore = true
|
||||
// Check if we're on a mobile device
|
||||
const isMobileDevice = () => {
|
||||
return (('ontouchstart' in window) ||
|
||||
(navigator.maxTouchPoints > 0) ||
|
||||
(navigator.msMaxTouchPoints > 0));
|
||||
};
|
||||
|
||||
const selection = document.getSelection()
|
||||
const text = selection.toString()
|
||||
// Check if we're on iOS
|
||||
const isIOS = () => {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
};
|
||||
|
||||
if (text === '') return
|
||||
const range = selection.getRangeAt(0)
|
||||
// Clear selection handler
|
||||
const clearSelection = () => {
|
||||
if (document.getSelection) {
|
||||
document.getSelection().removeAllRanges();
|
||||
}
|
||||
fn('cleared');
|
||||
};
|
||||
|
||||
const [contents] = rendition.getContents()
|
||||
const cfiRange = contents.cfiFromRange(range)
|
||||
// Process text selection
|
||||
const processSelection = (e) => {
|
||||
if (e.ignore) return;
|
||||
e.ignore = true;
|
||||
|
||||
const SelectionReact = range.getBoundingClientRect()
|
||||
const viewRect = rendition.manager.container.getBoundingClientRect()
|
||||
const selection = document.getSelection();
|
||||
const text = selection.toString();
|
||||
|
||||
if (text === '') return;
|
||||
|
||||
try {
|
||||
const range = selection.getRangeAt(0);
|
||||
const [contents] = rendition.getContents();
|
||||
const cfiRange = contents.cfiFromRange(range);
|
||||
|
||||
const SelectionReact = range.getBoundingClientRect();
|
||||
const viewRect = rendition.manager.container.getBoundingClientRect();
|
||||
|
||||
let react = {
|
||||
left: `${
|
||||
|
|
@ -32,7 +61,99 @@ export default function selectListener(document, rendition, fn) {
|
|||
top: `${viewRect.y + SelectionReact.y}px`,
|
||||
width: `${SelectionReact.width}px`,
|
||||
height: `${SelectionReact.height}px`,
|
||||
};
|
||||
|
||||
fn('selected', react, text, cfiRange);
|
||||
} catch (error) {
|
||||
console.error('Error processing selection:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Mouse events for desktop
|
||||
document.addEventListener('mousedown', clearSelection);
|
||||
|
||||
document.addEventListener('mouseup', (e) => {
|
||||
processSelection(e);
|
||||
});
|
||||
|
||||
// Only add touch events if we're on a touch device
|
||||
if (isMobileDevice()) {
|
||||
// Touch events for mobile
|
||||
document.addEventListener('touchstart', (e) => {
|
||||
touchStarted = true;
|
||||
touchSelection = false;
|
||||
touchStartTime = Date.now();
|
||||
|
||||
// Store start position for determining if it's a tap or selection attempt
|
||||
if (e.touches && e.touches[0]) {
|
||||
startX = e.touches[0].clientX;
|
||||
startY = e.touches[0].clientY;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchmove', (e) => {
|
||||
if (!touchStarted) return;
|
||||
|
||||
// Check if this might be a selection attempt (not just a tap)
|
||||
if (e.touches && e.touches[0]) {
|
||||
const moveX = Math.abs(e.touches[0].clientX - startX);
|
||||
const moveY = Math.abs(e.touches[0].clientY - startY);
|
||||
|
||||
// If user has moved finger more than threshold, might be trying to select
|
||||
if (moveX > TOUCH_MOVE_THRESHOLD || moveY > TOUCH_MOVE_THRESHOLD) {
|
||||
touchSelection = true;
|
||||
}
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchend', (e) => {
|
||||
if (!touchStarted) return;
|
||||
touchStarted = false;
|
||||
|
||||
const touchDuration = Date.now() - touchStartTime;
|
||||
|
||||
// Check if it was a long press or a deliberate selection movement
|
||||
if (touchDuration > LONG_PRESS_DURATION || touchSelection) {
|
||||
// Delay processing to allow the browser to complete the selection
|
||||
setTimeout(() => {
|
||||
processSelection(e);
|
||||
}, 50);
|
||||
} else {
|
||||
// It was a quick tap, clear selection
|
||||
clearSelection();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle context menu long press on mobile
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
// This event fires on long-press on many mobile browsers
|
||||
// Prevent the default context menu
|
||||
if (isMobileDevice()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Delay to let the browser create the selection
|
||||
setTimeout(() => {
|
||||
processSelection(e);
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// Add specific handling for iOS
|
||||
if (isIOS()) {
|
||||
// iOS sometimes needs additional help with text selection
|
||||
document.addEventListener('selectionchange', () => {
|
||||
// Only process if we're in a touch selection operation
|
||||
if (touchSelection || (Date.now() - touchStartTime) > LONG_PRESS_DURATION) {
|
||||
// Delay to let selection complete
|
||||
setTimeout(() => {
|
||||
const selection = document.getSelection();
|
||||
if (selection && selection.toString().length > 0) {
|
||||
const e = { ignore: false };
|
||||
processSelection(e);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
fn('selected', react, text, cfiRange)
|
||||
})
|
||||
}
|
||||
248
src/utils/listeners/touch.ts
Normal file
248
src/utils/listeners/touch.ts
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
/**
|
||||
* Unified touch handler that supports both swipe navigation and text selection
|
||||
* @param {Document} document - The document to add event listeners to
|
||||
* @param {Object} rendition - The EPUBJS rendition
|
||||
* @param {function} navigationFn - Function to call for navigation (swipe)
|
||||
* @param {function} selectionFn - Function to call for text selection
|
||||
*/
|
||||
|
||||
type epubEvent = TouchEvent & { ignore?: boolean };
|
||||
type Direction = 'next' | 'prev' | 'up' | 'down';
|
||||
|
||||
export default function touchHandler(
|
||||
document: Document,
|
||||
rendition: any,
|
||||
navigationFn: (direction: Direction) => void,
|
||||
selectionFn: (type: string, rect?: any, text?: string, cfiRange?: string) => void
|
||||
) {
|
||||
// State tracking
|
||||
let touchState = {
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
startTime: 0,
|
||||
isLongPress: false,
|
||||
isSelectionAttempt: false,
|
||||
lastTapTime: 0,
|
||||
moveCount: 0,
|
||||
lastMoveX: 0,
|
||||
lastMoveY: 0
|
||||
};
|
||||
|
||||
// Constants for gesture detection
|
||||
const SWIPE = {
|
||||
threshold: 50, // Min distance for swipe
|
||||
restraint: 200, // Max perpendicular movement
|
||||
allowedTime: 500 // Max time for swipe
|
||||
};
|
||||
|
||||
const SELECTION = {
|
||||
longPressDuration: 500, // Time for long press
|
||||
moveThreshold: 10, // Movement to trigger selection mode
|
||||
selectionDelay: 50 // Delay to process selection after touch
|
||||
};
|
||||
|
||||
// Clear selection
|
||||
const clearSelection = () => {
|
||||
if (document.getSelection) {
|
||||
document.getSelection()?.removeAllRanges();
|
||||
}
|
||||
selectionFn('cleared');
|
||||
};
|
||||
|
||||
// Process text selection
|
||||
const processSelection = () => {
|
||||
const selection = document.getSelection();
|
||||
if (!selection) return;
|
||||
|
||||
const text = selection.toString();
|
||||
if (text === '') return;
|
||||
|
||||
try {
|
||||
const range = selection.getRangeAt(0);
|
||||
const [contents] = rendition.getContents();
|
||||
const cfiRange = contents.cfiFromRange(range);
|
||||
|
||||
const selectionRect = range.getBoundingClientRect();
|
||||
const viewRect = rendition.manager.container.getBoundingClientRect();
|
||||
|
||||
const rect = {
|
||||
left: `${
|
||||
viewRect.x + selectionRect.x - (rendition.manager.scrollLeft || 0)
|
||||
}px`,
|
||||
top: `${viewRect.y + selectionRect.y}px`,
|
||||
width: `${selectionRect.width}px`,
|
||||
height: `${selectionRect.height}px`,
|
||||
};
|
||||
|
||||
selectionFn('selected', rect, text, cfiRange);
|
||||
} catch (error) {
|
||||
console.error('Error processing selection:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if device is iOS
|
||||
const isIOS = () => {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
};
|
||||
|
||||
// Device-specific adjustments
|
||||
if (isIOS()) {
|
||||
SELECTION.longPressDuration = 400;
|
||||
SELECTION.selectionDelay = 100;
|
||||
}
|
||||
|
||||
// Touch start event
|
||||
document.addEventListener('touchstart', (e: epubEvent) => {
|
||||
if (e.ignore) return;
|
||||
e.ignore = true;
|
||||
|
||||
// Get initial touch position
|
||||
if (e.touches && e.touches[0]) {
|
||||
touchState.startX = e.touches[0].pageX;
|
||||
touchState.startY = e.touches[0].pageY;
|
||||
touchState.lastMoveX = touchState.startX;
|
||||
touchState.lastMoveY = touchState.startY;
|
||||
touchState.startTime = Date.now();
|
||||
touchState.isLongPress = false;
|
||||
touchState.isSelectionAttempt = false;
|
||||
touchState.moveCount = 0;
|
||||
|
||||
// Handle long press with timeout
|
||||
const longPressTimeout = setTimeout(() => {
|
||||
// If finger hasn't moved much, trigger long press
|
||||
const currentX = touchState.lastMoveX;
|
||||
const currentY = touchState.lastMoveY;
|
||||
const moveX = Math.abs(currentX - touchState.startX);
|
||||
const moveY = Math.abs(currentY - touchState.startY);
|
||||
|
||||
if (moveX < SELECTION.moveThreshold && moveY < SELECTION.moveThreshold) {
|
||||
touchState.isLongPress = true;
|
||||
}
|
||||
}, SELECTION.longPressDuration);
|
||||
|
||||
// Store the timeout in a property to clear it if needed
|
||||
(document as any)._longPressTimeout = longPressTimeout;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Touch move event
|
||||
document.addEventListener('touchmove', (e: epubEvent) => {
|
||||
if (e.ignore) return;
|
||||
e.ignore = true;
|
||||
|
||||
if (e.touches && e.touches[0]) {
|
||||
touchState.lastMoveX = e.touches[0].pageX;
|
||||
touchState.lastMoveY = e.touches[0].pageY;
|
||||
touchState.moveCount++;
|
||||
|
||||
// Calculate movement
|
||||
const moveX = Math.abs(touchState.lastMoveX - touchState.startX);
|
||||
const moveY = Math.abs(touchState.lastMoveY - touchState.startY);
|
||||
|
||||
// Determine if this might be a selection attempt
|
||||
if (moveX > SELECTION.moveThreshold || moveY > SELECTION.moveThreshold) {
|
||||
// Clear long press timeout if significant movement
|
||||
if ((document as any)._longPressTimeout) {
|
||||
clearTimeout((document as any)._longPressTimeout);
|
||||
(document as any)._longPressTimeout = null;
|
||||
}
|
||||
|
||||
// If moved a lot horizontally relative to vertically, might be a swipe
|
||||
// If moved a lot vertically or in a pattern, might be selection
|
||||
if ((moveY > moveX * 1.5) || touchState.moveCount > 5) {
|
||||
touchState.isSelectionAttempt = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Touch end event
|
||||
document.addEventListener('touchend', (e: epubEvent) => {
|
||||
if (e.ignore) return;
|
||||
e.ignore = true;
|
||||
|
||||
// Clear long press timeout if it's still active
|
||||
if ((document as any)._longPressTimeout) {
|
||||
clearTimeout((document as any)._longPressTimeout);
|
||||
(document as any)._longPressTimeout = null;
|
||||
}
|
||||
|
||||
// Calculate touch stats
|
||||
const touchEndTime = Date.now();
|
||||
const elapsedTime = touchEndTime - touchState.startTime;
|
||||
|
||||
// Get distance traveled
|
||||
const distX = e.changedTouches[0].pageX - touchState.startX;
|
||||
const distY = e.changedTouches[0].pageY - touchState.startY;
|
||||
|
||||
// Handle selection if it was a long press or selection attempt
|
||||
if (touchState.isLongPress || touchState.isSelectionAttempt) {
|
||||
// Delay slightly to let the browser finish selection
|
||||
setTimeout(() => {
|
||||
processSelection();
|
||||
}, SELECTION.selectionDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle swipe if it wasn't a selection attempt
|
||||
if (elapsedTime <= SWIPE.allowedTime) {
|
||||
// Horizontal swipe
|
||||
if (Math.abs(distX) >= SWIPE.threshold && Math.abs(distY) <= SWIPE.restraint) {
|
||||
// If dist traveled is negative, it indicates right swipe
|
||||
navigationFn(distX < 0 ? 'next' : 'prev');
|
||||
}
|
||||
// Vertical swipe
|
||||
else if (Math.abs(distY) >= SWIPE.threshold && Math.abs(distX) <= SWIPE.restraint) {
|
||||
// If dist traveled is negative, it indicates up swipe
|
||||
navigationFn(distY < 0 ? 'up' : 'down');
|
||||
}
|
||||
// Tap - convert to click for regular interaction
|
||||
else {
|
||||
clearSelection();
|
||||
|
||||
// Convert tap to click
|
||||
document.dispatchEvent(
|
||||
new MouseEvent('click', {
|
||||
clientX: touchState.startX,
|
||||
clientY: touchState.startY,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// Handle context menu (long press on many mobile browsers)
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
// Only handle on mobile devices
|
||||
if ('ontouchstart' in window) {
|
||||
e.preventDefault();
|
||||
// Delay to let the browser create the selection
|
||||
setTimeout(() => {
|
||||
processSelection();
|
||||
}, SELECTION.selectionDelay);
|
||||
}
|
||||
});
|
||||
|
||||
// Add specific handling for iOS
|
||||
if (isIOS()) {
|
||||
// iOS sometimes needs additional help with text selection
|
||||
document.addEventListener('selectionchange', () => {
|
||||
// Only process if it might be from a touch operation
|
||||
if (touchState.isLongPress || touchState.isSelectionAttempt) {
|
||||
setTimeout(() => {
|
||||
const selection = document.getSelection();
|
||||
if (selection && selection.toString().length > 0) {
|
||||
processSelection();
|
||||
}
|
||||
}, SELECTION.selectionDelay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Regular mouse events for desktop
|
||||
document.addEventListener('mousedown', clearSelection);
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
processSelection();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue