Compare commits

...

1 commit

Author SHA1 Message Date
jrosh
8d52773c5f
combine select and swipe listeners, fix phone select 2025-05-30 12:30:06 +02:00
4 changed files with 431 additions and 50 deletions

View file

@ -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 = () => {

View file

@ -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
}

View file

@ -1,38 +1,159 @@
// 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;
// Check if we're on a mobile device
const isMobileDevice = () => {
return (('ontouchstart' in window) ||
(navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints > 0));
};
// Check if we're on iOS
const isIOS = () => {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
};
// Clear selection handler
const clearSelection = () => {
if (document.getSelection) {
document.getSelection().removeAllRanges();
}
fn('cleared');
};
// Process text selection
const processSelection = (e) => {
if (e.ignore) return;
e.ignore = true;
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: `${
viewRect.x + SelectionReact.x - (rendition.manager.scrollLeft || 0)
}px`,
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) => {
if (e.ignore) return
e.ignore = true
processSelection(e);
});
const selection = document.getSelection()
const text = selection.toString()
// 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 });
if (text === '') return
const range = selection.getRangeAt(0)
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 });
const [contents] = rendition.getContents()
const cfiRange = contents.cfiFromRange(range)
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();
}
});
const SelectionReact = range.getBoundingClientRect()
const viewRect = rendition.manager.container.getBoundingClientRect()
let react = {
left: `${
viewRect.x + SelectionReact.x - (rendition.manager.scrollLeft || 0)
}px`,
top: `${viewRect.y + SelectionReact.y}px`,
width: `${SelectionReact.width}px`,
height: `${SelectionReact.height}px`,
// 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)
})
}
}
}

View 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();
});
}