Files

4949 lines
223 KiB
JavaScript
Raw Permalink Normal View History

2026-09-24 13:05:20 -04:00
// Local Media Handler
export default class LocalMediaHandler {
constructor() {
this._clickHandler = null;
this._touchHandler = null;
this._fileChangeHandler = null;
this._globalClickHandler = null;
}
setupMediaView() {
// Find the active tab's media content div
const activeTabId = window.bibleApp?.activeTabId;
if (!activeTabId) {
console.error('[ERROR] No active tab found');
return;
}
const mediaContentDiv = document.getElementById(`mediaContent${activeTabId}`);
if (!mediaContentDiv) {
console.error('[ERROR] Media content div not found for tab:', activeTabId);
return;
}
const mediaContent = `
<div class="media-workspace">
<div class="media-thumbnails-section">
<div class="media-scroll-container">
<div class="media-card add-files" id="refreshMediaBtn" title="Click to add media files">
<input type="file" id="fileInput" multiple style="display: none" accept="image/*,video/*,audio/*,.pdf"/>
<div class="add-icon" style="font-size: 32px; line-height: 1; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0; padding: 0; overflow: visible;">+</div>
</div>
</div>
</div>
<div class="media-canvas" id="mediaCanvas">
<div class="canvas-content">
<!-- Placeholder removed per user request -->
</div>
</div>
</div>
<style>
.media-workspace {
display: flex;
flex-direction: column;
min-height: 100vh;
background: #000000;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.media-thumbnails-section {
flex: 0 0 120px;
background: #000000;
border-bottom: 1px solid #3d3d3d;
padding: 10px;
overflow: hidden;
position: relative;
z-index: 2;
pointer-events: auto;
}
.media-scroll-container {
display: flex;
gap: 10px;
overflow-x: auto;
overflow-y: hidden;
height: 100%;
padding-bottom: 10px;
white-space: nowrap;
}
.media-scroll-container::-webkit-scrollbar {
display: none;
}
.media-scroll-container {
-ms-overflow-style: none;
scrollbar-width: none;
}
.media-card {
flex: 0 0 100px;
height: 100px;
background: transparent;
border-radius: 0;
overflow: hidden;
cursor: grab !important;
transition: none;
position: relative;
user-select: none;
-webkit-user-drag: element;
pointer-events: auto !important;
z-index: 1;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.media-card:hover {
background: transparent;
transform: none;
cursor: grab !important;
}
.media-card.dragging {
opacity: 0.5;
cursor: grabbing !important;
z-index: 1000;
}
.media-card.add-files {
cursor: pointer !important;
border: none;
display: flex !important;
align-items: center;
justify-content: center;
background: transparent;
pointer-events: auto !important;
z-index: 10000 !important;
position: relative;
min-height: 100px;
min-width: 100px;
overflow: visible;
visibility: visible !important;
opacity: 1 !important;
transform: none !important;
}
.media-card.add-files * {
pointer-events: auto !important;
cursor: pointer !important;
}
.media-card.add-files .add-icon {
font-size: 32px !important;
line-height: 1 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
height: 100% !important;
text-align: center !important;
vertical-align: middle !important;
margin: 0 !important;
padding: 0 !important;
color: #6d6d6d !important;
font-weight: bold !important;
user-select: none !important;
overflow: visible !important;
position: relative !important;
z-index: 11 !important;
transform: translateY(-12px) !important;
}
.media-card.add-files:hover {
border: none;
background: transparent;
transform: none !important;
}
.add-icon {
font-size: 36px;
color: #6d6d6d;
font-weight: bold;
line-height: 1;
user-select: none;
transition: color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
text-align: center;
vertical-align: middle;
margin: 0;
padding: 0;
}
.media-card.add-files:hover .add-icon {
color: #8d8d8d;
}
.media-thumbnail {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
pointer-events: none;
}
.media-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
.media-thumbnail .file-icon {
font-size: 32px;
color: #fff;
pointer-events: none;
}
.media-canvas {
flex: 1;
position: relative;
overflow: auto;
background: #000000;
min-height: calc(100vh - 120px);
pointer-events: auto !important;
z-index: 1;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
scrollbar-width: none;
-ms-overflow-style: none;
}
.media-canvas::-webkit-scrollbar {
display: none;
width: 0;
background: transparent;
}
.media-canvas.drag-over {
border: none;
background: transparent;
pointer-events: auto !important;
}
.canvas-content {
min-height: 100%;
position: relative;
padding: 0;
pointer-events: auto !important;
overflow: auto;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
scrollbar-width: none;
-ms-overflow-style: none;
}
.canvas-content::-webkit-scrollbar {
display: none;
width: 0;
background: transparent;
}
/* Placeholder styles removed per user request */
.canvas-media-item {
position: absolute !important;
/* Removed min-width and min-height to prevent invisible borders */
background: transparent;
border-radius: 0;
overflow: hidden;
cursor: grab;
box-shadow: none;
user-select: none;
z-index: 100 !important; /* Base layer for media items */
pointer-events: auto !important;
touch-action: none;
/* Ensure container is exactly the size of content */
margin: 0;
padding: 0;
border: none;
display: block;
}
.canvas-media-item:hover {
cursor: grab;
}
.canvas-media-item:active {
cursor: grabbing;
}
.canvas-media-item img {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: auto;
}
.canvas-media-item iframe {
width: 100%;
height: 100%;
border: none;
pointer-events: auto;
}
.media-drawing-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: auto;
background: transparent;
cursor: crosshair;
z-index: 5000; /* Always above media items */
touch-action: none;
}
.media-drawing-canvas.drawing-disabled {
pointer-events: none;
cursor: default;
}
</style>
`;
// Replace the content of the media content div
mediaContentDiv.innerHTML = mediaContent;
// Setup event listeners after content is inserted
setTimeout(() => {
this.setupEventListeners();
// Also setup drawing canvas for the active tab
const activeTabId = window.bibleApp?.activeTabId;
if (activeTabId) {
this.setupMediaDrawingCanvas(activeTabId);
}
}, 100);
}
setupMediaDrawingCanvas(tabId) {
const mediaCanvas = document.getElementById('mediaCanvas');
if (!mediaCanvas) {
console.error('[ERROR] Media canvas not found');
return;
}
// Remove any existing drawing canvas
const existingDrawingCanvas = mediaCanvas.querySelector('.media-drawing-canvas');
if (existingDrawingCanvas) {
// Also remove its window 'resize' listener so it doesn't leak/accumulate
if (existingDrawingCanvas._resizeHandler) {
try { window.removeEventListener('resize', existingDrawingCanvas._resizeHandler); } catch (_) {}
}
existingDrawingCanvas.remove();
}
// Create drawing canvas
const drawingCanvas = document.createElement('canvas');
drawingCanvas.className = 'media-drawing-canvas';
drawingCanvas.id = `mediaDrawingCanvas${tabId}`;
// Add to canvas content instead of media canvas for proper positioning
const canvasContent = mediaCanvas.querySelector('.canvas-content');
if (canvasContent) {
canvasContent.appendChild(drawingCanvas);
} else {
// Fallback to media canvas if content not found
mediaCanvas.appendChild(drawingCanvas);
}
// Create an overlay canvas for in-progress strokes (prevents alpha stacking)
let overlayCanvas = document.getElementById(`mediaDrawingOverlay${tabId}`);
if (!overlayCanvas) {
overlayCanvas = document.createElement('canvas');
overlayCanvas.className = 'media-drawing-overlay';
overlayCanvas.id = `mediaDrawingOverlay${tabId}`;
// Always place overlay above drawing canvas
(canvasContent || mediaCanvas).appendChild(overlayCanvas);
}
// Initialize overlay size and context to match base canvas immediately
try {
const rect = (canvasContent || mediaCanvas).getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
overlayCanvas.width = rect.width * dpr;
overlayCanvas.height = rect.height * dpr;
overlayCanvas.style.position = 'absolute';
overlayCanvas.style.top = '0';
overlayCanvas.style.left = '0';
overlayCanvas.style.width = '100%';
overlayCanvas.style.height = '100%';
overlayCanvas.style.pointerEvents = 'none';
overlayCanvas.style.zIndex = '6000';
const octx = overlayCanvas.getContext('2d');
octx.setTransform(1, 0, 0, 1, 0, 0);
octx.scale(dpr, dpr);
} catch (e) {
console.warn('[MEDIA DRAW] Failed to init overlay size:', e);
}
// Set canvas size after it's in the DOM
this.resizeMediaDrawingCanvas(tabId);
// Setup drawing functionality
this.setupMediaDrawingListeners(drawingCanvas, tabId);
console.log('[MEDIA DRAW] setupMediaDrawingCanvas complete:', { tabId, hasOverlay: !!document.getElementById(`mediaDrawingOverlay${tabId}`) });
// Add resize listener
const resizeHandler = () => {
// Avoid resizing while actively drawing to prevent flicker/disappearing lines
if (typeof isDrawing !== 'undefined' && isDrawing) {
return;
}
this.resizeMediaDrawingCanvas(tabId);
// Keep overlay canvas in sync with main drawing canvas size
const overlay = document.getElementById(`mediaDrawingOverlay${tabId}`);
if (overlay) {
const rect = drawingCanvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
overlay.width = rect.width * dpr;
overlay.height = rect.height * dpr;
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.pointerEvents = 'none';
overlay.style.zIndex = '6000';
const octx = overlay.getContext('2d');
octx.setTransform(1, 0, 0, 1, 0, 0);
octx.scale(dpr, dpr);
}
};
window.addEventListener('resize', resizeHandler);
// Store resize handler for cleanup
drawingCanvas._resizeHandler = resizeHandler;
// Set up canvas styling for the new canvas
this.updateCanvasStyling(drawingCanvas);
// Set up tool change listener
this.setupToolChangeListener(drawingCanvas, tabId);
console.log('[DEBUG] Drawing canvas setup complete for tab:', tabId);
}
updateCanvasStyling(canvas) {
const isDrawingTool = window.currentTool === 'pen' ||
window.currentTool === 'highlight' ||
window.currentTool === 'erase';
const isSelectTool = window.currentTool === 'select';
// Position the drawing canvas over the canvas content area
canvas.style.position = 'absolute';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.background = 'transparent';
// 🎨 ENHANCED LAYERING SYSTEM: Canvas (annotations) ALWAYS on top, media items always below
// This ensures annotations are always visible above media elements when moved/resized
canvas.style.display = 'block';
canvas.style.setProperty('display', 'block', 'important');
// 🎨 Use proper layer manager for z-index
if (window.mediaLayerManager) {
window.mediaLayerManager.ensureAnnotationsOnTop();
} else {
// Fallback to proper annotation z-index
canvas.style.zIndex = '5000';
canvas.style.setProperty('z-index', '5000', 'important');
}
// Control interactivity through pointer-events, not z-index
if (isDrawingTool) {
// Drawing tools: Canvas is interactive, media items are not
canvas.style.pointerEvents = 'auto';
canvas.style.setProperty('pointer-events', 'auto', 'important');
canvas.style.cursor = 'crosshair';
canvas._selectModeActive = false;
console.log('[DEBUG] Drawing tool: Canvas interactive on top, media below');
} else if (isSelectTool) {
// Select tool: Canvas is transparent to events, media items are interactive
canvas.style.pointerEvents = 'none';
canvas.style.setProperty('pointer-events', 'none', 'important');
canvas.style.cursor = 'default';
canvas._selectModeActive = true;
console.log('[DEBUG] Select tool: Canvas visible on top but transparent, media interactive below');
} else {
// Default state: Canvas transparent, media items interactive
canvas.style.pointerEvents = 'none';
canvas.style.setProperty('pointer-events', 'none', 'important');
canvas.style.cursor = 'default';
canvas._selectModeActive = false;
console.log('[DEBUG] Default tool: Canvas transparent, media interactive');
}
// Update media items to work with the fixed layering system
this.updateMediaItemsPointerEvents(isDrawingTool, isSelectTool);
console.log('[DEBUG] 🎨 ENHANCED Canvas styling - display:', canvas.style.display, 'z-index:', canvas.style.zIndex, 'pointer-events:', canvas.style.pointerEvents, 'tool:', window.currentTool);
}
updateMediaItemsPointerEvents(isDrawingTool, isSelectTool) {
const mediaItems = document.querySelectorAll('.canvas-media-item');
console.log('[DEBUG] updateMediaItemsPointerEvents called with', mediaItems.length, 'items, isDrawingTool:', isDrawingTool, 'isSelectTool:', isSelectTool);
mediaItems.forEach(item => {
if (isDrawingTool) {
// When drawing tools are active, completely disable all interactions on media items
item.style.pointerEvents = 'none';
item.style.setProperty('pointer-events', 'none', 'important');
item.style.cursor = 'default';
item.style.userSelect = 'none';
item.style.webkitUserSelect = 'none';
item.style.setProperty('user-select', 'none', 'important');
item.style.setProperty('-webkit-user-select', 'none', 'important');
// Also disable any event listeners by temporarily removing and re-adding them
item.setAttribute('data-drawing-mode', 'true');
// Force the item to be completely non-interactive
item.style.setProperty('pointer-events', 'none', 'important');
item.style.setProperty('cursor', 'default', 'important');
item.style.setProperty('user-select', 'none', 'important');
item.style.setProperty('-webkit-user-select', 'none', 'important');
item.style.setProperty('-moz-user-select', 'none', 'important');
item.style.setProperty('-ms-user-select', 'none', 'important');
// CRITICAL: Also disable pointer events on all child elements (images, iframes)
const childElements = item.querySelectorAll('*');
childElements.forEach(child => {
child.style.pointerEvents = 'none';
child.style.setProperty('pointer-events', 'none', 'important');
});
// Ensure media items are always below annotation canvas while drawing
item.style.zIndex = '100';
item.style.setProperty('z-index', '100', 'important');
console.log('[LAYERING] Drawing tool - forced media z-index to 100 during drawing');
console.log('[DEBUG] Disabled interactions on media item for drawing (z-index: 100)');
} else if (isSelectTool) {
// CRITICAL: Remove data-drawing-mode FIRST before setting any styles
item.removeAttribute('data-drawing-mode');
// When select tool is active, enable pointer events for media manipulation
item.style.pointerEvents = 'auto';
item.style.setProperty('pointer-events', 'auto', 'important');
item.style.cursor = 'move';
item.style.userSelect = 'none';
item.style.webkitUserSelect = 'none';
item.style.setProperty('user-select', 'none', 'important');
item.style.setProperty('-webkit-user-select', 'none', 'important');
// Re-enable pointer events on child elements for selection and force cursor
const childElements = item.querySelectorAll('*');
childElements.forEach(child => {
child.style.pointerEvents = 'auto';
child.style.setProperty('pointer-events', 'auto', 'important');
child.style.setProperty('cursor', 'move', 'important');
});
// Keep media below canvas even in select mode; interaction happens via pointer-events
item.style.zIndex = '100';
item.style.setProperty('z-index', '100', 'important');
console.log('[LAYERING] Select tool - forced media z-index to 100');
console.log('[DEBUG] Enabled pointer events and interactive handlers on media item for selection (z-index: 100)');
console.log('[DEBUG] data-drawing-mode removed:', !item.hasAttribute('data-drawing-mode'));
// CRITICAL: Force the cursor to be visible and correct
item.style.setProperty('cursor', 'move', 'important');
} else {
// Default state - enable pointer events
item.style.pointerEvents = 'auto';
item.style.setProperty('pointer-events', 'auto', 'important');
item.style.cursor = 'grab';
item.style.userSelect = 'none';
item.style.webkitUserSelect = 'none';
item.style.setProperty('user-select', 'none', 'important');
item.style.setProperty('-webkit-user-select', 'none', 'important');
item.removeAttribute('data-drawing-mode');
// Re-enable pointer events on child elements
const childElements = item.querySelectorAll('*');
childElements.forEach(child => {
child.style.pointerEvents = 'auto';
child.style.setProperty('pointer-events', 'auto', 'important');
});
// PRESERVE LAYERING: Don't reset z-index if item was brought to front
const currentZIndex = parseInt(item.style.zIndex) || 100;
if (currentZIndex <= 100) {
// Only set to 100 if it's not already at a higher value from bringMediaToTop
item.style.zIndex = '100';
item.style.setProperty('z-index', '100', 'important');
}
console.log(`[LAYERING] Default state - preserved z-index: ${item.style.zIndex} for item`);
console.log('[DEBUG] Set default state for media item (z-index: 100)');
}
// Re-initialize media item for select tool to ensure event listeners are proper
if (isSelectTool) {
this.makeMediaItemInteractive(item);
console.log('[DEBUG] Re-initialized media item for select tool');
}
});
}
setupToolChangeListener(canvas, tabId) {
const originalUpdateToolUI = window.updateToolUI;
if (originalUpdateToolUI) {
window.updateToolUI = function() {
originalUpdateToolUI();
// Update canvas styling
if (this.updateCanvasStyling) {
this.updateCanvasStyling(canvas);
}
// Also update media items when tool changes
const isDrawingTool = window.currentTool === 'pen' ||
window.currentTool === 'highlight' ||
window.currentTool === 'erase';
const isSelectTool = window.currentTool === 'select';
if (this.updateMediaItemsPointerEvents) {
this.updateMediaItemsPointerEvents(isDrawingTool, isSelectTool);
}
// Robust recovery: if coming from select tool, ensure drawing works immediately
if (isDrawingTool) {
// Remove any lingering select-mode CSS override
const mediaSelectStyle = document.getElementById('media-select-tool-override');
if (mediaSelectStyle) {
mediaSelectStyle.remove();
console.log('[RECOVER] Removed lingering media select CSS override');
}
const computedPE = (canvas && window.getComputedStyle) ? getComputedStyle(canvas).pointerEvents : '';
if (canvas && (canvas._selectModeDisabled || computedPE === 'none')) {
canvas._selectModeDisabled = false;
canvas.style.setProperty('pointer-events', 'auto', 'important');
if (this.setupMediaDrawingListeners) {
this.setupMediaDrawingListeners(canvas, tabId);
console.log('[RECOVER] Reattached drawing listeners after leaving select');
}
if (this.updateCanvasStyling) {
this.updateCanvasStyling(canvas);
}
// Ensure interactions reflect drawing mode
if (this.refreshMediaInteractions) {
this.refreshMediaInteractions();
}
}
}
console.log('[DEBUG] Tool change listener updated - Tool:', window.currentTool, 'Drawing:', isDrawingTool, 'Select:', isSelectTool);
}.bind(this);
}
}
// Add a method to force refresh all media interactions
refreshMediaInteractions() {
const mediaItems = document.querySelectorAll('.canvas-media-item');
console.log('[DEBUG] Refreshing interactions for', mediaItems.length, 'media items, current tool:', window.currentTool);
// REMOVED: Force update canvas styling - app.js now controls this
// const drawingCanvases = document.querySelectorAll('.media-drawing-canvas');
// drawingCanvases.forEach(canvas => {
// this.updateCanvasStyling(canvas);
// });
// Update media items pointer events
const isDrawingTool = window.currentTool === 'pen' ||
window.currentTool === 'highlight' ||
window.currentTool === 'erase';
const isSelectTool = window.currentTool === 'select';
this.updateMediaItemsPointerEvents(isDrawingTool, isSelectTool);
// AGGRESSIVE FIX: If select tool is active, force apply cursor immediately
if (isSelectTool && window.selectModeActive) {
console.log('[AGGRESSIVE] Select tool is active - force applying cursor to all media items');
// Apply CSS override immediately
let selectToolStyle = document.getElementById('select-tool-cursor-override');
if (!selectToolStyle) {
selectToolStyle = document.createElement('style');
selectToolStyle.id = 'select-tool-cursor-override';
document.head.appendChild(selectToolStyle);
}
selectToolStyle.textContent = `
.canvas-media-item, .canvas-media-item * {
cursor: move !important;
pointer-events: auto !important;
}
.media-drawing-canvas {
pointer-events: none !important;
}
`;
// Force apply to all existing media items
mediaItems.forEach(item => {
item.style.setProperty('cursor', 'move', 'important');
item.style.setProperty('pointer-events', 'auto', 'important');
// Also apply to child elements
const childElements = item.querySelectorAll('*');
childElements.forEach(child => {
child.style.setProperty('cursor', 'move', 'important');
child.style.setProperty('pointer-events', 'auto', 'important');
});
});
console.log('[AGGRESSIVE] Forced select tool cursor on', mediaItems.length, 'media items');
// NUCLEAR OPTION: Force cursor with short delay to ensure it sticks
setTimeout(() => {
mediaItems.forEach(item => {
item.style.setProperty('cursor', 'move', 'important');
item.style.setProperty('pointer-events', 'auto', 'important');
const childElements = item.querySelectorAll('*');
childElements.forEach(child => {
child.style.setProperty('cursor', 'move', 'important');
child.style.setProperty('pointer-events', 'auto', 'important');
});
});
console.log('[NUCLEAR] Re-applied select cursor with delay to ensure it sticks');
}, 50);
}
// Re-setup each media item
mediaItems.forEach(item => {
// Clear any existing event listeners to prevent duplicates
if (item._mouseDownHandler) {
item.removeEventListener('mousedown', item._mouseDownHandler);
}
if (item._mouseMoveHandler) {
document.removeEventListener('mousemove', item._mouseMoveHandler);
}
if (item._mouseUpHandler) {
document.removeEventListener('mouseup', item._mouseUpHandler);
}
// Re-setup the item with current tool settings
this.makeMediaItemInteractive(item);
});
console.log('[DEBUG] Media interactions refresh complete');
}
resizeMediaDrawingCanvas(tabId) {
const drawingCanvas = document.getElementById(`mediaDrawingCanvas${tabId}`);
if (!drawingCanvas) {
console.error('[ERROR] Drawing canvas not found for resize');
return;
}
const mediaCanvas = document.getElementById('mediaCanvas');
if (!mediaCanvas) {
console.error('[ERROR] Media canvas not found for resize');
return;
}
const canvasContent = mediaCanvas.querySelector('.canvas-content');
if (!canvasContent) {
console.error('[ERROR] Canvas content not found for resize');
return;
}
// Wait a moment for the canvas content to be properly sized
setTimeout(() => {
const rect = canvasContent.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
// Set canvas size - ensure minimum dimensions and cover entire content area
const width = Math.max(rect.width, 300);
const height = Math.max(rect.height, 150);
drawingCanvas.width = width * dpr;
drawingCanvas.height = height * dpr;
drawingCanvas.style.width = '100%';
drawingCanvas.style.height = '100%';
// Position the drawing canvas directly over the canvas content
drawingCanvas.style.position = 'absolute';
drawingCanvas.style.top = '0';
drawingCanvas.style.left = '0';
drawingCanvas.style.pointerEvents = 'auto';
drawingCanvas.style.setProperty('pointer-events', 'auto', 'important');
// Always keep annotations on top of media items
drawingCanvas.style.zIndex = '5000';
drawingCanvas.style.setProperty('z-index', '5000', 'important');
drawingCanvas.style.cursor = 'crosshair';
drawingCanvas.style.background = 'transparent';
const ctx = drawingCanvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// Restore any existing strokes
this.restoreMediaStrokes(tabId);
// Also mirror stroke to unified history for global undo/redo
try {
if (window.getCurrentPassageKey && window.pushToUnifiedHistory) {
const passageKey = window.getCurrentPassageKey();
const strokeOperation = {
type: (strokeData.tool === 'erase') ? 'erase' : 'draw',
timestamp: Date.now(),
tabId: window.bibleApp?.activeTabId,
passageKey,
isStroke: true,
strokeData
};
window.pushToUnifiedHistory(strokeOperation);
}
} catch(_) {}
console.log('[DEBUG] Drawing canvas resized successfully:', width, 'x', height, 'position: 0,0 (100% coverage)');
}, 50);
}
setupMediaDrawingListeners(canvas, tabId) {
let isDrawing = false;
let lastPoint = null;
let currentStrokePoints = [];
const overlay = document.getElementById(`mediaDrawingOverlay${tabId}`);
const overlayCtx = overlay ? overlay.getContext('2d') : null;
const baseCtx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
let overlayPathOpen = false; // retained for compatibility but not used with redraw strategy
const getPointerPos = (e) => {
const rect = canvas.getBoundingClientRect();
const canvasContent = canvas.parentElement; // Canvas is now direct child of canvas-content
const dpr = window.devicePixelRatio || 1;
if (canvasContent && canvasContent.classList.contains('canvas-content')) {
const contentRect = canvasContent.getBoundingClientRect();
const scrollLeft = canvasContent.scrollLeft || 0;
const scrollTop = canvasContent.scrollTop || 0;
// Calculate position relative to the drawing canvas (which is now at 0,0 relative to content)
const x = (e.clientX - contentRect.left + scrollLeft);
const y = (e.clientY - contentRect.top + scrollTop);
return { x, y };
} else {
// Fallback to original calculation
return {
x: (e.clientX - rect.left),
y: (e.clientY - rect.top)
};
}
};
const getDrawingSettings = () => {
// Use the main app's drawing settings function
if (window.getCurrentDrawingSettings) {
return window.getCurrentDrawingSettings();
}
// Fallback to basic settings if main app function not available
const currentTool = window.currentTool || 'pen';
switch(currentTool) {
case 'pen':
return { color: '#ffff00', size: 3, alpha: 1.0, composite: 'source-over' };
case 'highlight':
return { color: 'rgba(255,255,0,0.30)', size: 15, alpha: 0.3, composite: 'source-over' };
case 'erase':
return { color: '#000000', size: 24, alpha: 1.0, composite: 'destination-out' };
default:
return { color: '#ffff00', size: 3, alpha: 1.0, composite: 'source-over' };
}
};
const startDrawing = (e) => {
console.log('[DEBUG] startDrawing called, tool:', window.currentTool, 'canvas._selectModeActive:', canvas._selectModeActive);
// Don't draw if select tool is active
if (window.currentTool === 'select' || canvas._selectModeActive) {
console.log('[DEBUG] Select tool active - not drawing');
return;
}
// Auto-close the slide-out tool rail when a drawing action begins on media
try {
const isDrawingTool = (window.currentTool === 'pen' || window.currentTool === 'highlight' || window.currentTool === 'erase');
if (isDrawingTool && !window.penSizeAdjustActive && typeof window.closeToolRail === 'function') {
window.closeToolRail();
}
} catch (_) {}
// Clear any previous residue on the live overlay for a fresh stroke
if (overlayCtx && overlay) {
overlayCtx.clearRect(0, 0, overlay.width, overlay.height);
}
const point = getPointerPos(e);
// Guard: ignore extremely short taps/clicks with negligible movement and duration
// This prevents transient overlay clear/restore flashes from producing visible shrink/jump
if (e && e.timeStamp && typeof window.performance !== 'undefined') {
canvas._lastDownTs = e.timeStamp;
}
console.log('[DEBUG] Drawing point:', point);
// Check if we're over a media item - if so, allow drawing on it
const mediaItems = document.querySelectorAll('.canvas-media-item');
let drawingOnMediaItem = null;
for (const item of mediaItems) {
const itemRect = item.getBoundingClientRect();
const canvasContent = canvas.parentElement; // Canvas is now direct child of canvas-content
if (canvasContent && canvasContent.classList.contains('canvas-content')) {
const contentRect = canvasContent.getBoundingClientRect();
const scrollLeft = canvasContent.scrollLeft || 0;
const scrollTop = canvasContent.scrollTop || 0;
// Calculate item position relative to canvas content (same coordinate system as drawing)
const itemLeft = itemRect.left - contentRect.left + scrollLeft;
const itemTop = itemRect.top - contentRect.top + scrollTop;
const itemRight = itemLeft + itemRect.width;
const itemBottom = itemTop + itemRect.height;
if (point.x >= itemLeft && point.x <= itemRight &&
point.y >= itemTop && point.y <= itemBottom) {
drawingOnMediaItem = item;
console.log('[DEBUG] Drawing on media item:', item.dataset.mediaPath);
console.log('[DEBUG] Point:', point, 'Item bounds:', { left: itemLeft, top: itemTop, right: itemRight, bottom: itemBottom });
break;
}
} else {
// Fallback to original calculation
const canvasRect = canvas.getBoundingClientRect();
if (point.x >= (itemRect.left - canvasRect.left) &&
point.x <= (itemRect.right - canvasRect.left) &&
point.y >= (itemRect.top - canvasRect.top) &&
point.y <= (itemRect.bottom - canvasRect.top)) {
drawingOnMediaItem = item;
console.log('[DEBUG] Drawing on media item (fallback):', item.dataset.mediaPath);
break;
}
}
}
isDrawing = true;
lastPoint = point;
currentStrokePoints = [point];
// Store which media item we're drawing on (if any)
canvas._drawingOnMediaItem = drawingOnMediaItem;
const settings = getDrawingSettings();
// Live preview on overlay (prevents alpha stacking artifacts during draw)
if (overlayCtx && settings) {
// Prepare overlay state (clear fully in device pixels)
const dpr = window.devicePixelRatio || 1;
overlayCtx.setTransform(1, 0, 0, 1, 0, 0);
overlayCtx.clearRect(0, 0, overlay.width, overlay.height);
// Use devicePixelRatio transform and keep coordinates in CSS pixels (no / dpr)
overlayCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
// Show eraser visual when erasing
if (window.currentTool === 'erase' && typeof window.updateEraserVisualAid === 'function') {
const ex = e.clientX || (e.touches && e.touches[0]?.clientX) || 0;
const ey = e.clientY || (e.touches && e.touches[0]?.clientY) || 0;
window.updateEraserVisualAid(ex, ey, settings.size);
}
// Ensure base canvas remains untouched during draw; do not modify its transform
// The base context retains devicePixelRatio scaling from initialization
console.log('[DEBUG] Drawing started successfully');
};
const draw = (e) => {
if (!isDrawing) {
console.log('[DEBUG] draw called but not drawing');
return;
}
const point = getPointerPos(e);
currentStrokePoints.push(point);
// Draw on overlay using the same redraw strategy as bible tabs
if (overlayCtx && currentStrokePoints.length >= 2) {
const settings = getDrawingSettings();
const dpr = window.devicePixelRatio || 1;
// Reset and clear in device pixels
overlayCtx.setTransform(1, 0, 0, 1, 0, 0);
overlayCtx.clearRect(0, 0, overlay.width, overlay.height);
// Apply DPR scaling and draw using CSS-pixel coordinates (no division)
overlayCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
overlayCtx.save();
overlayCtx.globalAlpha = settings.alpha;
overlayCtx.globalCompositeOperation = settings.composite;
overlayCtx.strokeStyle = settings.color;
overlayCtx.lineWidth = settings.composite === 'source-over' && (window.currentTool === 'highlight') ? Math.max(settings.size, 12) : settings.size;
overlayCtx.lineCap = 'round';
overlayCtx.lineJoin = 'round';
overlayCtx.beginPath();
overlayCtx.moveTo(currentStrokePoints[0].x, currentStrokePoints[0].y);
const wasTouch = (window.currentDrawingInputType === 'touch');
if (wasTouch) {
for (let i = 1; i < currentStrokePoints.length - 1; i++) {
const midX = (currentStrokePoints[i].x + currentStrokePoints[i + 1].x) / 2;
const midY = (currentStrokePoints[i].y + currentStrokePoints[i + 1].y) / 2;
overlayCtx.quadraticCurveTo(
currentStrokePoints[i].x,
currentStrokePoints[i].y,
midX,
midY
);
}
overlayCtx.lineTo(
currentStrokePoints[currentStrokePoints.length - 1].x,
currentStrokePoints[currentStrokePoints.length - 1].y
);
} else {
for (let i = 1; i < currentStrokePoints.length; i++) {
overlayCtx.lineTo(currentStrokePoints[i].x, currentStrokePoints[i].y);
}
}
overlayCtx.stroke();
overlayCtx.restore();
}
if (window.currentTool === 'erase' && typeof window.updateEraserVisualAid === 'function') {
const ex = e.clientX || (e.touches && e.touches[0]?.clientX) || 0;
const ey = e.clientY || (e.touches && e.touches[0]?.clientY) || 0;
window.updateEraserVisualAid(ex, ey, getDrawingSettings().size);
}
lastPoint = point;
};
const endDrawing = () => {
if (!isDrawing) return;
isDrawing = false;
lastPoint = null;
// Save stroke data for persistence
const elapsedMs = (canvas._lastDownTs && typeof performance !== 'undefined') ? (performance.now() - canvas._lastDownTs) : null;
const minimalMovement = currentStrokePoints.length <= 1;
const isVeryShort = elapsedMs !== null ? elapsedMs < 30 : false;
if (currentStrokePoints.length > 1 && !isVeryShort) {
const strokeData = {
points: currentStrokePoints,
tool: window.currentTool || 'pen',
settings: getDrawingSettings(),
timestamp: Date.now()
};
// If we were drawing on a media item, associate the stroke with it
if (canvas._drawingOnMediaItem) {
strokeData.mediaItemId = canvas._drawingOnMediaItem.dataset.mediaPath;
}
this.saveMediaStroke(tabId, strokeData);
// Close the live path and clear overlay before final render
if (overlayCtx && overlay) {
overlayCtx.setTransform(1, 0, 0, 1, 0, 0);
overlayCtx.clearRect(0, 0, overlay.width, overlay.height);
}
console.log('[MEDIA DRAW] Stroke saved - points:', currentStrokePoints.length, 'tool:', strokeData.tool);
this.restoreMediaStrokes(tabId);
} else {
// Extremely short strokes: just clear overlay without triggering a full restore redraw
if (overlayCtx && overlay) {
overlayCtx.clearRect(0, 0, overlay.width, overlay.height);
}
}
currentStrokePoints = [];
canvas._drawingOnMediaItem = null;
overlayPathOpen = false;
// Hide eraser visual when ending draw
if (typeof window.hideEraserVisualOnToolChange === 'function') {
window.hideEraserVisualOnToolChange();
}
};
// Initialize canvas select mode flag
canvas._selectModeActive = false;
// Remove any existing listeners first
canvas.removeEventListener('mousedown', canvas._mouseDownHandler);
canvas.removeEventListener('mousemove', canvas._mouseMoveHandler);
canvas.removeEventListener('mouseup', canvas._mouseUpHandler);
canvas.removeEventListener('mouseleave', canvas._mouseLeaveHandler);
canvas.removeEventListener('touchstart', canvas._touchStartHandler);
canvas.removeEventListener('touchmove', canvas._touchMoveHandler);
canvas.removeEventListener('touchend', canvas._touchEndHandler);
// Create handler functions and store them for potential removal
canvas._mouseDownHandler = (e) => {
e.preventDefault();
e.stopPropagation();
startDrawing(e);
};
canvas._mouseMoveHandler = (e) => {
if (isDrawing) {
e.preventDefault();
e.stopPropagation();
draw(e);
}
};
canvas._mouseUpHandler = (e) => {
e.preventDefault();
e.stopPropagation();
endDrawing();
};
canvas._mouseLeaveHandler = (e) => {
endDrawing();
};
canvas._touchStartHandler = (e) => {
// Allow two-finger gestures to pass through to media items even when drawing tools are active
if (e.touches && e.touches.length >= 2) {
canvas._multiTouchOverride = true;
// Temporarily disable drawing canvas so underlying media receives the touches
canvas.style.setProperty('pointer-events', 'none', 'important');
// Also explicitly enable pointer events on media items beneath
try {
document.querySelectorAll('.canvas-media-item').forEach(el => el.style.setProperty('pointer-events', 'auto', 'important'));
} catch(_) {}
return; // Do NOT preventDefault/stopPropagation for multi-touch
}
e.preventDefault();
e.stopPropagation();
if (e.touches.length === 1) {
startDrawing(e.touches[0]);
}
};
canvas._touchMoveHandler = (e) => {
if (isDrawing && e.touches.length === 1) {
e.preventDefault();
e.stopPropagation();
draw(e.touches[0]);
}
};
canvas._touchEndHandler = (e) => {
// If we were in multi-touch override, restore drawing when touches reduce
if (canvas._multiTouchOverride) {
if (!e.touches || e.touches.length <= 1) {
canvas._multiTouchOverride = false;
canvas.style.setProperty('pointer-events', 'auto', 'important');
// No change to media items; they keep pointer-events auto
}
return; // Let media handle the end of gesture
}
e.preventDefault();
e.stopPropagation();
endDrawing();
};
// Always add drawing listeners; pointer-events of the canvas is managed elsewhere
console.log('[DEBUG] Adding drawing event listeners');
// Mouse events
canvas.addEventListener('mousedown', canvas._mouseDownHandler);
canvas.addEventListener('mousemove', canvas._mouseMoveHandler);
canvas.addEventListener('mouseup', canvas._mouseUpHandler);
canvas.addEventListener('mouseleave', canvas._mouseLeaveHandler);
// Touch events (non-passive to allow preventDefault during draw)
canvas.addEventListener('touchstart', (ev) => {
// If canvas was disabled by select tool earlier, re-enable on first touch for resilience
if (canvas._selectModeDisabled && window.currentTool !== 'select') {
console.log('[RESILIENT] Re-enabling drawing canvas on touchstart');
canvas._selectModeDisabled = false;
canvas.style.setProperty('pointer-events', 'auto', 'important');
canvas.style.setProperty('z-index', '5000', 'important');
// Ensure listeners are up to date
if (window.localMediaHandler && typeof window.localMediaHandler.setupMediaDrawingListeners === 'function') {
window.localMediaHandler.setupMediaDrawingListeners(canvas, tabId);
}
}
canvas._touchStartHandler(ev);
}, { passive: false });
canvas.addEventListener('touchmove', canvas._touchMoveHandler, { passive: false });
canvas.addEventListener('touchend', canvas._touchEndHandler, { passive: false });
// Two-finger override: while pen/highlight/erase is active, allow two-finger gestures
// to manipulate underlying media by temporarily disabling the drawing canvas
if (canvas._pointerDownCapture) {
window.removeEventListener('pointerdown', canvas._pointerDownCapture, true);
window.removeEventListener('pointerup', canvas._pointerUpCapture, true);
window.removeEventListener('pointercancel', canvas._pointerUpCapture, true);
}
canvas._activeTouchPointers = new Set();
canvas._pointerDownCapture = (e) => {
if (e.pointerType !== 'touch') return;
canvas._activeTouchPointers.add(e.pointerId);
if (canvas._activeTouchPointers.size >= 2) {
// End any ongoing stroke and let pinch/drag pass through
if (isDrawing) {
try { endDrawing(); } catch(_) {}
}
canvas._multiTouchOverride = true;
canvas.style.setProperty('pointer-events', 'none', 'important');
}
};
canvas._pointerUpCapture = (e) => {
if (e.pointerType !== 'touch') return;
canvas._activeTouchPointers.delete(e.pointerId);
if (canvas._activeTouchPointers.size <= 1) {
// Restore drawing once multi-touch ends
canvas._multiTouchOverride = false;
canvas.style.setProperty('pointer-events', 'auto', 'important');
}
};
window.addEventListener('pointerdown', canvas._pointerDownCapture, true);
window.addEventListener('pointerup', canvas._pointerUpCapture, true);
window.addEventListener('pointercancel', canvas._pointerUpCapture, true);
}
saveMediaStroke(tabId, strokeData) {
if (!window.bibleApp) return;
const tab = window.bibleApp.tabs.find(t => t.id === tabId);
if (!tab) return;
if (!tab.mediaStrokes) {
tab.mediaStrokes = [];
}
// If stroke already has a mediaItemId, use that
if (strokeData.mediaItemId) {
const mediaItem = document.querySelector(`[data-media-path="${strokeData.mediaItemId}"]`);
if (mediaItem) {
// Transform coordinates to be relative to the media item
const transformedStroke = this.transformStrokeToMediaItem(strokeData, mediaItem);
transformedStroke.mediaItemId = strokeData.mediaItemId;
tab.mediaStrokes.push(transformedStroke);
console.log('[DEBUG] Saved media stroke associated with media item:', strokeData.mediaItemId);
} else {
// Media item not found, save as background stroke
strokeData.mediaItemId = null;
tab.mediaStrokes.push(strokeData);
console.log('[DEBUG] Media item not found, saved as background stroke');
}
} else {
// Find which media item this stroke belongs to (if any)
const mediaItem = this.findMediaItemAtPosition(strokeData.points[0], tabId);
if (mediaItem) {
// Transform coordinates to be relative to the media item
const transformedStroke = this.transformStrokeToMediaItem(strokeData, mediaItem);
transformedStroke.mediaItemId = mediaItem.dataset.mediaPath; // Use path as unique ID
tab.mediaStrokes.push(transformedStroke);
console.log('[DEBUG] Saved media stroke associated with media item:', mediaItem.dataset.mediaPath);
} else {
// Stroke is on the background canvas
strokeData.mediaItemId = null;
tab.mediaStrokes.push(strokeData);
console.log('[DEBUG] Saved background stroke');
}
}
console.log('[DEBUG] Saved media stroke to tab:', tabId, 'Total strokes:', tab.mediaStrokes.length);
}
findMediaItemAtPosition(point, tabId) {
const mediaItems = document.querySelectorAll('.canvas-media-item');
const canvas = document.getElementById('mediaCanvas');
if (!canvas) return null;
const canvasContent = canvas.querySelector('.canvas-content');
if (!canvasContent) return null;
const contentRect = canvasContent.getBoundingClientRect();
const scrollLeft = canvasContent.scrollLeft || 0;
const scrollTop = canvasContent.scrollTop || 0;
for (const item of mediaItems) {
const itemRect = item.getBoundingClientRect();
// Calculate item position relative to canvas content
const itemLeft = itemRect.left - contentRect.left + scrollLeft;
const itemTop = itemRect.top - contentRect.top + scrollTop;
const itemRight = itemLeft + itemRect.width;
const itemBottom = itemTop + itemRect.height;
// Check if point is within the media item bounds
if (point.x >= itemLeft && point.x <= itemRight &&
point.y >= itemTop && point.y <= itemBottom) {
return item;
}
}
return null;
}
transformStrokeToMediaItem(strokeData, mediaItem) {
const canvas = document.getElementById('mediaCanvas');
if (!canvas) return strokeData;
const canvasContent = canvas.querySelector('.canvas-content');
if (!canvasContent) return strokeData;
const contentRect = canvasContent.getBoundingClientRect();
const itemRect = mediaItem.getBoundingClientRect();
const scrollLeft = canvasContent.scrollLeft || 0;
const scrollTop = canvasContent.scrollTop || 0;
// Calculate the offset of the media item relative to the canvas content
const itemOffsetX = itemRect.left - contentRect.left + scrollLeft;
const itemOffsetY = itemRect.top - contentRect.top + scrollTop;
// Transform all points to be relative to the media item
const transformedPoints = strokeData.points.map(point => ({
x: point.x - itemOffsetX,
y: point.y - itemOffsetY
}));
return {
...strokeData,
points: transformedPoints,
originalPoints: strokeData.points // Keep original for reference
};
}
transformStrokeFromMediaItem(strokeData, mediaItem) {
const canvas = document.getElementById('mediaCanvas');
if (!canvas) return strokeData;
const canvasContent = canvas.querySelector('.canvas-content');
if (!canvasContent) return strokeData;
const contentRect = canvasContent.getBoundingClientRect();
const itemRect = mediaItem.getBoundingClientRect();
const scrollLeft = canvasContent.scrollLeft || 0;
const scrollTop = canvasContent.scrollTop || 0;
// Calculate the offset of the media item relative to the canvas content
const itemOffsetX = itemRect.left - contentRect.left + scrollLeft;
const itemOffsetY = itemRect.top - contentRect.top + scrollTop;
// Transform points back to canvas coordinates
const transformedPoints = strokeData.points.map(point => ({
x: point.x + itemOffsetX,
y: point.y + itemOffsetY
}));
return {
...strokeData,
points: transformedPoints
};
}
restoreMediaStrokes(tabId) {
console.log('[DEBUG] Restoring media strokes for tab:', tabId);
if (!window.bibleApp) return;
const tab = window.bibleApp.tabs.find(t => t.id === tabId);
if (!tab || !tab.mediaStrokes || tab.mediaStrokes.length === 0) {
console.log('[DEBUG] No media strokes to restore for tab:', tabId);
return;
}
const canvas = document.getElementById(`mediaDrawingCanvas${tabId}`);
if (!canvas) {
console.error('[ERROR] Media drawing canvas not found for restoration');
return;
}
const ctx = canvas.getContext('2d');
// Clear before redrawing to avoid opacity stacking from multiple restores
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.restore();
tab.mediaStrokes.forEach(strokeData => {
const settings = strokeData.settings;
// Draw each stroke with saved settings while preventing cumulative state
ctx.save();
ctx.globalCompositeOperation = settings.composite || 'source-over';
ctx.strokeStyle = settings.color;
ctx.lineWidth = settings.size;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalAlpha = typeof settings.alpha === 'number' ? settings.alpha : 1.0;
// Transform coordinates if this stroke is associated with a media item
let pointsToDraw = strokeData.points;
if (strokeData.mediaItemId) {
const mediaItem = document.querySelector(`[data-media-path="${strokeData.mediaItemId}"]`);
if (mediaItem) {
const transformedStroke = this.transformStrokeFromMediaItem(strokeData, mediaItem);
pointsToDraw = transformedStroke.points;
}
}
if (pointsToDraw.length > 1) {
ctx.beginPath();
ctx.moveTo(pointsToDraw[0].x, pointsToDraw[0].y);
for (let i = 1; i < pointsToDraw.length; i++) {
ctx.lineTo(pointsToDraw[i].x, pointsToDraw[i].y);
}
ctx.stroke();
}
ctx.restore();
});
console.log('[DEBUG] Restored', tab.mediaStrokes.length, 'media strokes for tab:', tabId);
}
updateAnnotationsForMovedMediaItem(movedItem) {
// SEAMLESS INTEGRATION: Real-time annotation updates during media item movement
console.log('[DEBUG] Updating annotations for moved media item');
const activeTabId = window.bibleApp?.activeTabId;
if (!activeTabId) {
console.warn('[WARN] No active tab ID found for annotation update');
return;
}
const canvas = document.getElementById(`mediaDrawingCanvas${activeTabId}`);
if (!canvas) {
console.warn('[WARN] Media drawing canvas not found for annotation update');
return;
}
// Clear the entire canvas
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Redraw all strokes with updated positions
this.restoreMediaStrokes(activeTabId);
console.log('[DEBUG] Annotations updated for media item movement');
}
setupEventListeners() {
console.log('[DEBUG] LocalMediaHandler: Setting up event listeners');
const refreshBtn = document.getElementById('refreshMediaBtn');
const fileInput = document.getElementById('fileInput');
console.log('[DEBUG] LocalMediaHandler: Button element found:', !!refreshBtn);
console.log('[DEBUG] LocalMediaHandler: File input found:', !!fileInput);
if (refreshBtn) {
console.log('[DEBUG] LocalMediaHandler: Button details:', {
id: refreshBtn.id,
className: refreshBtn.className,
tagName: refreshBtn.tagName,
innerHTML: refreshBtn.innerHTML.substring(0, 100) + '...',
style: {
pointerEvents: refreshBtn.style.pointerEvents,
cursor: refreshBtn.style.cursor,
display: refreshBtn.style.display,
visibility: refreshBtn.style.visibility
}
});
}
if (refreshBtn && fileInput) {
console.log('[DEBUG] LocalMediaHandler: Adding click listener to plus button');
// Remove any existing listeners first to prevent duplicates
if (this._clickHandler) {
refreshBtn.removeEventListener('click', this._clickHandler);
}
if (this._touchHandler) {
refreshBtn.removeEventListener('touchend', this._touchHandler);
}
// Simple, reliable event handling
this._clickHandler = (e) => {
console.log('[DEBUG] LocalMediaHandler: Plus button clicked, triggering file input');
console.log('[DEBUG] Event details:', {
type: e.type,
target: e.target,
currentTarget: e.currentTarget,
bubbles: e.bubbles,
cancelable: e.cancelable
});
console.log('[DEBUG] Button element:', refreshBtn);
console.log('[DEBUG] Button computed style:', window.getComputedStyle(refreshBtn));
console.log('[DEBUG] Button rect:', refreshBtn.getBoundingClientRect());
console.log('[DEBUG] Button is visible:', refreshBtn.offsetWidth > 0 && refreshBtn.offsetHeight > 0);
// Visual feedback to confirm click is working
refreshBtn.style.background = '#ff0000';
setTimeout(() => {
refreshBtn.style.background = '#2a2a2a';
}, 200);
e.preventDefault();
e.stopPropagation();
// ALWAYS CREATE A NEW FILE INPUT - this is the most reliable approach
try {
console.log('[DEBUG] Creating new file input for reliability');
const newFileInput = document.createElement('input');
newFileInput.type = 'file';
newFileInput.multiple = true;
newFileInput.accept = 'image/*,video/*,audio/*,.pdf';
newFileInput.style.position = 'absolute';
newFileInput.style.left = '-9999px';
newFileInput.style.top = '-9999px';
newFileInput.style.opacity = '0';
newFileInput.style.pointerEvents = 'auto';
// Add change listener
newFileInput.addEventListener('change', (changeEvent) => {
console.log('[DEBUG] New file input changed, files:', changeEvent.target.files);
this.handleFileSelection(changeEvent.target.files);
// Clean up
if (document.body.contains(newFileInput)) {
document.body.removeChild(newFileInput);
}
});
// Add to DOM and click
document.body.appendChild(newFileInput);
console.log('[DEBUG] New file input added to DOM, triggering click');
newFileInput.click();
console.log('[DEBUG] New file input click triggered successfully');
// Clean up after a delay if no files were selected
setTimeout(() => {
if (document.body.contains(newFileInput)) {
document.body.removeChild(newFileInput);
console.log('[DEBUG] Cleaned up unused file input');
}
}, 5000);
} catch (error) {
console.error('[ERROR] Failed to create and trigger new file input:', error);
alert('Failed to open file picker. Please try refreshing the page.');
}
};
this._touchHandler = (e) => {
console.log('[DEBUG] LocalMediaHandler: Plus button touched, triggering file input');
e.preventDefault();
e.stopPropagation();
// Use the same approach as click handler
this._clickHandler(e);
};
// SIMPLE DIRECT CLICK HANDLER - this will definitely work
// Add a flag to prevent multiple rapid clicks
let isFilePickerOpen = false;
const simpleClickHandler = (e) => {
console.log('[SIMPLE] Media button clicked!');
console.log('[SIMPLE] Event details:', e);
// Prevent multiple file pickers from opening
if (isFilePickerOpen) {
console.log('[SIMPLE] File picker already open, ignoring click');
return;
}
isFilePickerOpen = true;
try {
// Create a new file input and trigger it immediately
const newInput = document.createElement('input');
newInput.type = 'file';
newInput.multiple = true;
newInput.accept = 'image/*,video/*,audio/*,.pdf';
newInput.style.display = 'none';
console.log('[SIMPLE] Created new file input:', newInput);
newInput.addEventListener('change', (e) => {
console.log('[SIMPLE] Files selected:', e.target.files);
this.handleFileSelection(e.target.files);
if (document.body.contains(newInput)) {
try {
document.body.removeChild(newInput);
} catch (error) {
console.log('[SIMPLE] File input already removed');
}
}
isFilePickerOpen = false; // Reset flag when files are selected
});
// Also reset flag if user cancels (after a delay)
setTimeout(() => {
if (document.body.contains(newInput)) {
try {
document.body.removeChild(newInput);
console.log('[SIMPLE] File picker cancelled, reset flag');
} catch (error) {
console.log('[SIMPLE] File input already removed');
}
isFilePickerOpen = false;
} else {
isFilePickerOpen = false;
console.log('[SIMPLE] File picker cancelled, reset flag (input already removed)');
}
}, 1000);
document.body.appendChild(newInput);
console.log('[SIMPLE] File input added to DOM, triggering click...');
newInput.click();
console.log('[SIMPLE] File input click triggered successfully');
} catch (error) {
console.error('[SIMPLE] Error in simpleClickHandler:', error);
isFilePickerOpen = false; // Reset flag on error
}
};
// Remove all existing listeners first
refreshBtn.removeEventListener('click', this._clickHandler);
refreshBtn.removeEventListener('touchend', this._touchHandler);
refreshBtn.removeEventListener('mousedown', this._clickHandler);
refreshBtn.removeEventListener('mouseup', this._clickHandler);
refreshBtn.removeEventListener('click', simpleClickHandler);
refreshBtn.removeEventListener('touchend', simpleClickHandler);
// Clear any existing onclick
refreshBtn.onclick = null;
// Use ONLY the simple click handler - no multiple listeners
refreshBtn.onclick = simpleClickHandler;
// Store the simple click handler
this._simpleClickHandler = simpleClickHandler;
// REMOVED: Global click handler - not needed since direct onclick works
// The direct onclick handler is sufficient and prevents multiple triggers
this._globalClickHandler = null;
// Ensure the button is properly styled and accessible
refreshBtn.style.pointerEvents = 'auto';
refreshBtn.style.cursor = 'pointer';
refreshBtn.style.zIndex = '10000';
refreshBtn.style.position = 'relative';
refreshBtn.setAttribute('tabindex', '0');
refreshBtn.setAttribute('role', 'button');
refreshBtn.setAttribute('aria-label', 'Add media files');
refreshBtn.setAttribute('data-testid', 'media-plus-button');
// Force the button to be clickable
refreshBtn.style.display = 'block';
refreshBtn.style.visibility = 'visible';
refreshBtn.style.opacity = '1';
// CRITICAL: Override any CSS that might be interfering
refreshBtn.style.position = 'relative';
refreshBtn.style.zIndex = '999999';
refreshBtn.style.pointerEvents = 'auto';
refreshBtn.style.cursor = 'pointer';
refreshBtn.style.userSelect = 'none';
refreshBtn.style.webkitUserSelect = 'none';
refreshBtn.style.touchAction = 'manipulation';
// Ensure the add icon is also clickable
const addIcon = refreshBtn.querySelector('.add-icon');
if (addIcon) {
addIcon.style.pointerEvents = 'auto';
addIcon.style.cursor = 'pointer';
addIcon.style.zIndex = '999999';
}
// Add a simple test to see if the button is actually clickable
console.log('[DEBUG] Button final state:', {
pointerEvents: refreshBtn.style.pointerEvents,
cursor: refreshBtn.style.cursor,
zIndex: refreshBtn.style.zIndex,
display: refreshBtn.style.display,
visibility: refreshBtn.style.visibility,
opacity: refreshBtn.style.opacity,
offsetWidth: refreshBtn.offsetWidth,
offsetHeight: refreshBtn.offsetHeight
});
// Remove existing file input listener to prevent duplicates
if (this._fileChangeHandler) {
fileInput.removeEventListener('change', this._fileChangeHandler);
}
this._fileChangeHandler = (e) => {
console.log('[DEBUG] LocalMediaHandler: File input changed, files:', e.target.files);
this.handleFileSelection(e.target.files);
};
fileInput.addEventListener('change', this._fileChangeHandler);
console.log('[DEBUG] LocalMediaHandler: Event listeners added successfully');
// Set up a periodic check to ensure the button stays clickable
const ensureButtonClickable = () => {
if (refreshBtn) {
refreshBtn.style.pointerEvents = 'auto';
refreshBtn.style.cursor = 'pointer';
refreshBtn.style.zIndex = '999999';
refreshBtn.style.position = 'relative';
const addIcon = refreshBtn.querySelector('.add-icon');
if (addIcon) {
addIcon.style.pointerEvents = 'auto';
addIcon.style.cursor = 'pointer';
addIcon.style.zIndex = '999999';
}
}
};
// Run the check every second (deduped)
if (window._localMediaButtonIntervalId) {
try { clearInterval(window._localMediaButtonIntervalId); } catch (_) {}
}
window._localMediaButtonIntervalId = setInterval(ensureButtonClickable, 1000);
console.log('[DEBUG] LocalMediaHandler: Periodic button check started');
} else {
console.error('[ERROR] LocalMediaHandler: Could not find required elements');
console.error('[ERROR] Plus button (refreshMediaBtn):', refreshBtn);
console.error('[ERROR] File input (fileInput):', fileInput);
// Retry after a short delay if elements not found
setTimeout(() => {
console.log('[DEBUG] Retrying event listener setup...');
this.setupEventListeners();
}, 100);
}
// Setup drag and drop for canvas
const mediaCanvas = document.getElementById('mediaCanvas');
if (mediaCanvas) {
console.log('[DEBUG] Setting up canvas drag and drop');
// Prevent default drag behaviors
mediaCanvas.addEventListener('dragenter', (e) => {
console.log('[DEBUG] Canvas dragenter');
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
mediaCanvas.classList.add('drag-over');
});
mediaCanvas.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
mediaCanvas.classList.add('drag-over');
});
mediaCanvas.addEventListener('dragleave', (e) => {
console.log('[DEBUG] Canvas dragleave', e.relatedTarget);
e.preventDefault();
e.stopPropagation();
if (!mediaCanvas.contains(e.relatedTarget)) {
mediaCanvas.classList.remove('drag-over');
}
});
mediaCanvas.addEventListener('drop', (e) => {
console.log('[DEBUG] Canvas drop event', e);
e.preventDefault();
e.stopPropagation();
mediaCanvas.classList.remove('drag-over');
try {
const data = e.dataTransfer.getData('text/plain');
console.log('[DEBUG] Dropped data:', data);
if (!data) {
console.error('[ERROR] No data in drop event');
console.log('[DEBUG] Available data types:', e.dataTransfer.types);
console.log('[DEBUG] Data transfer items:', e.dataTransfer.items);
return;
}
const mediaData = JSON.parse(data);
console.log('[DEBUG] Parsed media data:', mediaData);
const canvasRect = mediaCanvas.getBoundingClientRect();
const x = e.clientX - canvasRect.left;
const y = e.clientY - canvasRect.top;
console.log('[DEBUG] Adding media at position:', { x, y });
this.addMediaToCanvas(mediaData, x, y, mediaCanvas);
} catch (err) {
console.error('[ERROR] Failed to parse dropped media data:', err);
console.error('[ERROR] Raw data:', e.dataTransfer.getData('text/plain'));
console.log('[DEBUG] Available data types:', e.dataTransfer.types);
console.log('[DEBUG] Data transfer items:', e.dataTransfer.items);
}
});
console.log('[DEBUG] Canvas drag and drop listeners added');
} else {
console.error('[ERROR] Media canvas not found for drag and drop setup');
}
// Add a test function to verify drag functionality
window.testDragFunctionality = () => {
console.log('[TEST] Testing drag functionality...');
const mediaCards = document.querySelectorAll('.media-card:not(.add-files)');
console.log('[TEST] Found media cards:', mediaCards.length);
mediaCards.forEach((card, index) => {
console.log(`[TEST] Card ${index}:`, {
title: card.title,
draggable: card.draggable,
dataset: card.dataset,
classList: card.classList.toString(),
style: {
cursor: card.style.cursor,
pointerEvents: card.style.pointerEvents,
zIndex: card.style.zIndex
}
});
// Test if drag events are attached
const dragStartEvents = getEventListeners(card, 'dragstart');
console.log(`[TEST] Card ${index} dragstart listeners:`, dragStartEvents);
});
const mediaCanvas = document.getElementById('mediaCanvas');
if (mediaCanvas) {
console.log('[TEST] Media canvas found:', {
id: mediaCanvas.id,
classList: mediaCanvas.classList.toString(),
style: {
pointerEvents: mediaCanvas.style.pointerEvents,
zIndex: mediaCanvas.style.zIndex
}
});
} else {
console.error('[TEST] Media canvas not found');
}
};
// Helper function to get event listeners (for debugging)
function getEventListeners(element, eventType) {
// This is a simplified version - in real browsers you'd need dev tools
return 'Event listeners attached (check dev tools for details)';
}
console.log('[DEBUG] Test function added: window.testDragFunctionality()');
// Add a test function for the plus button
window.testMediaPlusButton = () => {
console.log('[TEST] Testing media plus button functionality...');
const plusButton = document.getElementById('refreshMediaBtn');
const fileInput = document.getElementById('fileInput');
console.log('[TEST] Plus button found:', !!plusButton);
console.log('[TEST] File input found:', !!fileInput);
if (plusButton) {
console.log('[TEST] Plus button properties:', {
id: plusButton.id,
className: plusButton.className,
style: {
pointerEvents: plusButton.style.pointerEvents,
cursor: plusButton.style.cursor,
zIndex: plusButton.style.zIndex,
display: plusButton.style.display
},
onclick: !!plusButton.onclick,
eventListeners: 'Check dev tools for event listeners'
});
// Test click
console.log('[TEST] Triggering test click...');
plusButton.click();
} else {
console.error('[TEST] Plus button not found!');
}
};
console.log('[DEBUG] Test function added: window.testMediaPlusButton()');
// Add a test function for media items on canvas
window.testMediaItemsOnCanvas = () => {
console.log('[TEST] Testing media items on canvas...');
const mediaItems = document.querySelectorAll('.canvas-media-item');
console.log('[TEST] Found media items on canvas:', mediaItems.length);
mediaItems.forEach((item, index) => {
console.log(`[TEST] Media item ${index}:`, {
id: item.id,
className: item.className,
style: {
pointerEvents: item.style.pointerEvents,
cursor: item.style.cursor,
zIndex: item.style.zIndex,
left: item.style.left,
top: item.style.top,
width: item.style.width,
height: item.style.height
},
dataset: item.dataset,
eventListeners: 'Check dev tools for event listeners'
});
// Test if the item is clickable
console.log(`[TEST] Testing click on media item ${index}...`);
item.click();
});
};
console.log('[DEBUG] Test function added: window.testMediaItemsOnCanvas()');
// SUPER SIMPLE TEST FUNCTION - this will definitely work
window.forceOpenFilePicker = () => {
console.log('[FORCE] Forcing file picker to open...');
// Always create a new file input for maximum reliability
const newInput = document.createElement('input');
newInput.type = 'file';
newInput.multiple = true;
newInput.accept = 'image/*,video/*,audio/*,.pdf';
newInput.style.position = 'absolute';
newInput.style.left = '-9999px';
newInput.style.top = '-9999px';
newInput.style.opacity = '0';
newInput.addEventListener('change', (e) => {
console.log('[FORCE] Files selected:', e.target.files);
this.handleFileSelection(e.target.files);
if (document.body.contains(newInput)) {
document.body.removeChild(newInput);
}
});
document.body.appendChild(newInput);
console.log('[FORCE] New file input created and added to DOM');
newInput.click();
console.log('[FORCE] File input click triggered');
// Clean up after 5 seconds if no files selected
setTimeout(() => {
if (document.body.contains(newInput)) {
document.body.removeChild(newInput);
console.log('[FORCE] Cleaned up unused file input');
}
}, 5000);
};
console.log('[DEBUG] Force function added: window.forceOpenFilePicker()');
// Add global test function for select tool
window.testSelectTool = () => {
console.log('[TEST] Testing select tool functionality...');
console.log('[TEST] Current tool:', window.currentTool);
console.log('[TEST] Select mode active:', window.selectModeActive);
const mediaItems = document.querySelectorAll('.canvas-media-item');
console.log('[TEST] Found', mediaItems.length, 'media items');
mediaItems.forEach((item, index) => {
console.log(`[TEST] Media item ${index}:`, {
pointerEvents: item.style.pointerEvents,
cursor: item.style.cursor,
zIndex: item.style.zIndex,
hasMouseDownHandler: !!item._mouseDownHandler
});
});
// Force select tool
if (window.currentTool !== 'select') {
console.log('[TEST] Switching to select tool...');
window.currentTool = 'select';
if (window.localMediaHandler && window.localMediaHandler.refreshMediaInteractions) {
window.localMediaHandler.refreshMediaInteractions();
}
}
};
// Add function to force select tool on all media items
window.forceSelectTool = () => {
console.log('[FORCE] Forcing select tool on all media items...');
window.currentTool = 'select';
window.selectModeActive = true;
const mediaItems = document.querySelectorAll('.canvas-media-item');
console.log('[FORCE] Found', mediaItems.length, 'media items to update');
mediaItems.forEach((item, index) => {
// Force pointer events and cursor
item.style.pointerEvents = 'auto';
item.style.setProperty('pointer-events', 'auto', 'important');
item.style.setProperty('cursor', 'move', 'important');
// PRESERVE LAYERING: Don't reset z-index if item was brought to front
const currentZIndex = parseInt(item.style.zIndex) || 100;
if (currentZIndex <= 100) {
item.style.setProperty('z-index', '100', 'important');
}
console.log(`[LAYERING] forceSelectTool - preserved z-index: ${item.style.zIndex} for item`);
console.log(`[FORCE] Updated media item ${index}`);
});
// Force canvas to be transparent to events
const drawingCanvases = document.querySelectorAll('.media-drawing-canvas');
drawingCanvases.forEach(canvas => {
canvas.style.pointerEvents = 'none';
canvas.style.setProperty('pointer-events', 'none', 'important');
canvas.style.setProperty('z-index', '1', 'important');
});
// Refresh interactions
if (window.localMediaHandler && window.localMediaHandler.refreshMediaInteractions) {
window.localMediaHandler.refreshMediaInteractions();
}
console.log('[FORCE] Select tool forced on all media items');
};
// Add a universal function to ensure media interactions are working
window.ensureMediaInteractions = () => {
console.log('[ENSURE] Ensuring media interactions are working...');
// Make sure we have a local media handler
if (!window.localMediaHandler) {
console.log('[ENSURE] Creating local media handler...');
window.localMediaHandler = new LocalMediaHandler();
}
// Refresh all interactions
if (window.localMediaHandler.refreshMediaInteractions) {
window.localMediaHandler.refreshMediaInteractions();
}
// Force update canvas pointer events
if (window.updateCanvasPointerEvents) {
window.updateCanvasPointerEvents();
}
console.log('[ENSURE] Media interactions ensured');
};
// Add touch optimization test function
window.testTouchOptimizations = () => {
if (window.localMediaHandler && window.localMediaHandler.testTouchOptimizations) {
window.localMediaHandler.testTouchOptimizations();
} else {
console.log('❌ LocalMediaHandler not available');
}
};
console.log('[DEBUG] Test functions added: window.testSelectTool(), window.forceSelectTool(), window.ensureMediaInteractions(), and window.testTouchOptimizations()');
}
handleFileSelection(files) {
const container = document.querySelector('.media-scroll-container');
if (!container) return;
// Get current tab to save media thumbnails
const currentTab = window.bibleApp?.tabs?.find(t => t.id === window.bibleApp.activeTabId);
if (!currentTab) {
console.error('[ERROR] No active tab found for saving media thumbnails');
return;
}
// Initialize media thumbnails array if it doesn't exist
if (!currentTab.mediaThumbnails) {
currentTab.mediaThumbnails = [];
console.log('[DEBUG] Initialized mediaThumbnails array for tab:', currentTab.id);
}
Array.from(files).forEach(file => {
const extension = file.name.split('.').pop();
const fileType = this.getFileType(extension);
const fileIcon = this.getFileIcon(fileType);
// Create URL for the file
const fileUrl = URL.createObjectURL(file);
// Create media card
const card = document.createElement('div');
card.className = 'media-card';
card.dataset.mediaPath = fileUrl;
card.dataset.mediaType = fileType;
card.title = file.name;
// Create thumbnail
const thumbnail = document.createElement('div');
thumbnail.className = 'media-thumbnail';
if (fileType === 'image') {
const img = document.createElement('img');
img.src = fileUrl;
img.alt = file.name;
thumbnail.appendChild(img);
} else {
const icon = document.createElement('div');
icon.className = 'file-icon';
icon.textContent = fileIcon;
thumbnail.appendChild(icon);
}
card.appendChild(thumbnail);
container.appendChild(card);
// Save thumbnail data to tab
const thumbnailData = {
id: Date.now() + Math.random(), // Unique ID
path: fileUrl,
name: file.name,
type: fileType,
icon: fileIcon
};
currentTab.mediaThumbnails.push(thumbnailData);
console.log('[DEBUG] Saved media thumbnail to tab:', thumbnailData);
// Setup drag listeners for the new card
this.setupDragListeners(card);
console.log('[DEBUG] Drag listeners set up for card:', card.title);
});
console.log('[DEBUG] Tab now has', currentTab.mediaThumbnails.length, 'media thumbnails');
// Trigger save of media content to ensure persistence
if (window.bibleApp && window.bibleApp.activeTabId) {
const mediaContentDiv = document.getElementById(`mediaContent${window.bibleApp.activeTabId}`);
if (mediaContentDiv) {
// Update the tab's media content to include the new thumbnails
const tab = window.bibleApp.tabs.find(t => t.id === window.bibleApp.activeTabId);
if (tab) {
tab.mediaContent = mediaContentDiv.innerHTML;
console.log('[DEBUG] Updated tab media content after adding thumbnails');
}
}
}
}
setupDragListeners(card) {
let dragImage = null;
const createDragImage = () => {
const img = card.querySelector('img');
if (img) {
dragImage = img.cloneNode(true);
dragImage.style.position = 'absolute';
dragImage.style.left = '-9999px';
document.body.appendChild(dragImage);
return dragImage;
}
return null;
};
// Mouse drag events
card.setAttribute('draggable', 'true');
// Add some debugging to see if the card is properly set up
console.log('[DEBUG] Setting up drag for card:', {
title: card.title,
draggable: card.draggable,
dataset: card.dataset,
classList: card.classList.toString()
});
card.addEventListener('dragstart', (e) => {
console.log('[DEBUG] Drag start triggered', {
path: card.dataset.mediaPath,
type: card.dataset.mediaType,
name: card.title,
target: e.target,
currentTarget: e.currentTarget
});
// Set the drag data first
const data = JSON.stringify({
path: card.dataset.mediaPath,
type: card.dataset.mediaType,
name: card.title
});
try {
e.dataTransfer.setData('text/plain', data);
e.dataTransfer.effectAllowed = 'copy';
console.log('[DEBUG] Data set for drag:', data);
} catch (error) {
console.error('[ERROR] Failed to set drag data:', error);
}
// Add dragging class
card.classList.add('dragging');
// Create and set drag image for both images and PDFs
if (card.dataset.mediaType === 'image') {
const img = createDragImage();
if (img) {
e.dataTransfer.setDragImage(img, 0, 0);
}
} else {
// For PDFs and other files, use the card itself as drag image
e.dataTransfer.setDragImage(card, 0, 0);
}
// Don't prevent default - let the drag happen naturally
// e.preventDefault(); // Removed this line
});
card.addEventListener('dragend', (e) => {
console.log('[DEBUG] Drag end');
card.classList.remove('dragging');
if (dragImage) {
dragImage.remove();
dragImage = null;
}
});
// Add right-click context menu for deleting thumbnails
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
// Create context menu
const contextMenu = document.createElement('div');
contextMenu.className = 'media-context-menu';
contextMenu.style.cssText = `
position: fixed;
top: ${e.clientY}px;
left: ${e.clientX}px;
background: #2d2d2d;
border: 1px solid #3d3d3d;
border-radius: 4px;
padding: 8px 0;
z-index: 10000;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
`;
const deleteOption = document.createElement('div');
deleteOption.textContent = '🗑️ Delete';
deleteOption.style.cssText = `
padding: 8px 16px;
cursor: pointer;
color: #ff6b6b;
font-size: 14px;
transition: background 0.2s;
`;
deleteOption.addEventListener('mouseenter', () => {
deleteOption.style.background = '#3d3d3d';
});
deleteOption.addEventListener('mouseleave', () => {
deleteOption.style.background = 'transparent';
});
deleteOption.addEventListener('click', () => {
// Remove from tab data
this.removeMediaThumbnailFromTab(card.dataset.mediaPath, card.dataset.mediaType);
// Remove from DOM
card.remove();
// Remove context menu
document.body.removeChild(contextMenu);
console.log('[DEBUG] Deleted media thumbnail:', card.title);
});
contextMenu.appendChild(deleteOption);
document.body.appendChild(contextMenu);
// Remove context menu when clicking elsewhere
const removeContextMenu = () => {
if (document.body.contains(contextMenu)) {
document.body.removeChild(contextMenu);
}
document.removeEventListener('click', removeContextMenu);
document.removeEventListener('contextmenu', removeContextMenu);
};
setTimeout(() => {
document.addEventListener('click', removeContextMenu);
document.addEventListener('contextmenu', removeContextMenu);
}, 100);
});
// Add drag event to handle drag cancellation
card.addEventListener('drag', (e) => {
// This event fires during the drag operation
// We can use it to ensure the drag continues
if (e.dataTransfer.effectAllowed === 'none') {
e.dataTransfer.effectAllowed = 'copy';
}
});
// Touch drag events
let touchTimeout;
let isDragging = false;
let startX, startY;
let longPressTimeout;
let isLongPress = false;
card.addEventListener('touchstart', (e) => {
console.log('[DEBUG] Touch start on card:', card.title);
const touch = e.touches[0];
startX = touch.clientX;
startY = touch.clientY;
// Start long press timer for delete option
longPressTimeout = setTimeout(() => {
isLongPress = true;
// Show delete confirmation
if (confirm(`Delete "${card.title}"?`)) {
// Remove from tab data
this.removeMediaThumbnailFromTab(card.dataset.mediaPath, card.dataset.mediaType);
// Remove from DOM
card.remove();
console.log('[DEBUG] Deleted media thumbnail via long press:', card.title);
}
}, 1000); // 1 second long press
touchTimeout = setTimeout(() => {
isDragging = true;
card.classList.add('dragging');
console.log('[DEBUG] Touch drag started for card:', card.title);
}, 200);
}, { passive: true });
card.addEventListener('touchmove', (e) => {
// Clear long press timeout if moving
if (longPressTimeout) {
clearTimeout(longPressTimeout);
longPressTimeout = null;
}
if (!isDragging) {
clearTimeout(touchTimeout);
return;
}
e.preventDefault();
const touch = e.touches[0];
const mediaCanvas = document.getElementById('mediaCanvas');
// Check if touch is over the canvas
if (mediaCanvas) {
const canvasRect = mediaCanvas.getBoundingClientRect();
if (touch.clientX >= canvasRect.left &&
touch.clientX <= canvasRect.right &&
touch.clientY >= canvasRect.top &&
touch.clientY <= canvasRect.bottom) {
mediaCanvas.classList.add('drag-over');
} else {
mediaCanvas.classList.remove('drag-over');
}
}
}, { passive: false });
card.addEventListener('touchend', (e) => {
// Clear long press timeout
if (longPressTimeout) {
clearTimeout(longPressTimeout);
longPressTimeout = null;
}
clearTimeout(touchTimeout);
if (!isDragging) return;
console.log('[DEBUG] Touch drag ended for card:', card.title);
const touch = e.changedTouches[0];
const mediaCanvas = document.getElementById('mediaCanvas');
if (mediaCanvas) {
const canvasRect = mediaCanvas.getBoundingClientRect();
if (touch.clientX >= canvasRect.left &&
touch.clientX <= canvasRect.right &&
touch.clientY >= canvasRect.top &&
touch.clientY <= canvasRect.bottom) {
// Add media to canvas at touch point
const mediaData = {
path: card.dataset.mediaPath,
type: card.dataset.mediaType,
name: card.title
};
console.log('[DEBUG] Adding media via touch drag:', mediaData);
this.addMediaToCanvas(mediaData, touch.clientX - canvasRect.left, touch.clientY - canvasRect.top, mediaCanvas);
}
mediaCanvas.classList.remove('drag-over');
}
isDragging = false;
card.classList.remove('dragging');
});
// Add a visual indicator that the card is draggable
card.style.cursor = 'grab';
card.addEventListener('mouseenter', () => {
card.style.cursor = 'grab';
});
card.addEventListener('mouseleave', () => {
card.style.cursor = 'grab';
});
}
addMediaToCanvas(mediaData, x, y, canvas) {
console.log('[DEBUG] Adding media to canvas:', { mediaData, x, y });
// Placeholder removed per user request - no longer needed
// Create media item
const mediaItem = document.createElement('div');
mediaItem.className = 'canvas-media-item';
mediaItem.dataset.mediaPath = mediaData.path;
mediaItem.dataset.mediaType = mediaData.type; // Store the media type
mediaItem.style.left = x + 'px';
mediaItem.style.top = y + 'px';
// Build the media element via DOM APIs (never innerHTML) so a file name/path cannot
// break out of an attribute and inject markup/script (stored XSS).
if (mediaData.type === 'image') {
const img = document.createElement('img');
img.src = mediaData.path;
img.alt = mediaData.name || '';
img.style.cssText = 'width:100%; height:100%; object-fit:contain;';
mediaItem.appendChild(img);
} else {
// For PDFs and other files, create an iframe to display them
const iframe = document.createElement('iframe');
iframe.src = mediaData.path;
iframe.setAttribute('frameborder', '0');
iframe.style.cssText = 'width:100%; height:100%;';
mediaItem.appendChild(iframe);
}
// Add to canvas content
const canvasContent = canvas.querySelector('.canvas-content');
if (canvasContent) {
console.log('[DEBUG] Found canvas content, appending media item');
canvasContent.appendChild(mediaItem);
// Set initial size based on media type - ensure proper content sizing
if (mediaData.type === 'image') {
// For images, start with a reasonable size that matches content
mediaItem.style.width = '250px';
mediaItem.style.height = '200px';
// Ensure the image inside fits exactly and adjust container to match aspect ratio
const img = mediaItem.querySelector('img');
if (img) {
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'contain'; // Show full image without cropping
img.style.margin = '0';
img.style.padding = '0';
img.style.border = 'none';
img.style.display = 'block';
// Adjust container aspect ratio when image loads to eliminate empty space
img.onload = () => {
this.adjustContainerAspectRatio(mediaItem, img);
};
// If already loaded, adjust immediately
if (img.complete && img.naturalWidth > 0) {
this.adjustContainerAspectRatio(mediaItem, img);
}
}
} else {
// For PDFs, use a more reasonable initial size
mediaItem.style.width = '400px';
mediaItem.style.height = '500px';
// Ensure the iframe inside fits exactly
const iframe = mediaItem.querySelector('iframe');
if (iframe) {
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.margin = '0';
iframe.style.padding = '0';
iframe.style.border = 'none';
iframe.style.display = 'block';
}
}
this.makeMediaItemInteractive(mediaItem);
// CRITICAL FIX: Ensure proper initialization of interactions
setTimeout(() => {
this.makeMediaItemInteractive(mediaItem);
console.log('[DEBUG] Re-initialized media item interactions after creation');
}, 100);
// Save media item to tab data for persistence; stamp the DOM element with the
// persisted unique id so remove/update operations target the right record
// (the same file can be added multiple times, sharing path/type).
const savedMediaItem = this.saveMediaItemToTab(mediaData, x, y, mediaItem.style.width, mediaItem.style.height);
if (savedMediaItem && savedMediaItem.id != null) mediaItem.dataset.mediaId = String(savedMediaItem.id);
console.log('[DEBUG] Media item added and made interactive');
// CRITICAL FIX: Apply the same refresh mechanism that works during tab switching
// This ensures the select tool works immediately when media is first loaded
setTimeout(() => {
// Only refresh if we're NOT currently in select mode to avoid interference
if (window.currentTool !== 'select') {
if (this.refreshMediaInteractions) {
this.refreshMediaInteractions();
console.log('[DEBUG] Applied tab-switch-style refresh for first media load');
}
} else {
// If we're in select mode, just ensure the new item is properly set up
console.log('[DEBUG] Select tool is active - ensuring new media item is properly configured');
this.makeMediaItemInteractive(mediaItem);
this.updateMediaItemsPointerEvents(false, true);
// IMMEDIATE: Apply select tool cursor to new media item
mediaItem.style.setProperty('cursor', 'move', 'important');
mediaItem.style.setProperty('pointer-events', 'auto', 'important');
// Also apply to child elements
const childElements = mediaItem.querySelectorAll('*');
childElements.forEach(child => {
child.style.setProperty('cursor', 'move', 'important');
child.style.setProperty('pointer-events', 'auto', 'important');
});
console.log('[CURSOR FIX] Applied select tool cursor to new media item immediately');
// Apply the CSS override if it doesn't exist
let selectToolStyle = document.getElementById('select-tool-cursor-override');
if (!selectToolStyle) {
selectToolStyle = document.createElement('style');
selectToolStyle.id = 'select-tool-cursor-override';
document.head.appendChild(selectToolStyle);
selectToolStyle.textContent = `
.canvas-media-item, .canvas-media-item * {
cursor: move !important;
pointer-events: auto !important;
}
.media-drawing-canvas {
pointer-events: none !important;
}
`;
console.log('[CURSOR FIX] Created CSS override for new media item');
}
}
// Always force update tool UI to ensure proper state
if (window.updateToolUI) {
window.updateToolUI();
}
// FINAL AGGRESSIVE FIX: Force select cursor again after everything is done
setTimeout(() => {
if (window.currentTool === 'select') {
console.log('[MEDIA ADDED] Final cursor fix for new media item');
if (window.fixSelectToolCursor) {
window.fixSelectToolCursor();
}
}
}, 100);
}, 200); // Same timing as tab switch refresh
// Ensure drawing canvas is properly set up and immediately ready for drawing after media is added
const activeTabId = window.bibleApp?.activeTabId;
if (activeTabId) {
// Ensure drawing canvas exists and is properly set up
this.ensureDrawingCanvasExists(activeTabId);
// Immediately update canvas styling for current tool and pointer events
const drawingCanvas = document.getElementById(`mediaDrawingCanvas${activeTabId}`);
if (drawingCanvas && this.updateCanvasStyling) {
this.updateCanvasStyling(drawingCanvas);
}
// Proactively re-apply the current drawing tool so pen/highlighter work without a tab switch
const isDrawingTool = window.currentTool === 'pen' || window.currentTool === 'highlight' || window.currentTool === 'erase';
if (isDrawingTool && typeof window.forceSetDrawingTool === 'function') {
window.forceSetDrawingTool(window.currentTool);
}
// Sync global pointer events and media interactions
if (typeof window.updateCanvasPointerEvents === 'function') {
window.updateCanvasPointerEvents();
}
if (this.refreshMediaInteractions) {
// Short delay to avoid race conditions with DOM updates
setTimeout(() => this.refreshMediaInteractions(), 10);
}
// Force resize the drawing canvas to ensure proper positioning
setTimeout(() => {
this.resizeMediaDrawingCanvas(activeTabId);
}, 100);
}
} else {
console.error('[ERROR] Could not find canvas content');
}
}
// Adjust container aspect ratio to match image (eliminates empty space without cropping)
adjustContainerAspectRatio(mediaItem, img) {
try {
const naturalWidth = img.naturalWidth;
const naturalHeight = img.naturalHeight;
if (naturalWidth === 0 || naturalHeight === 0) {
console.log('[DEBUG] Image not loaded yet, skipping aspect ratio adjustment');
return;
}
// Calculate image aspect ratio
const imageAspect = naturalWidth / naturalHeight;
// Get current container size as base
const currentWidth = parseInt(mediaItem.style.width) || 250;
const currentHeight = parseInt(mediaItem.style.height) || 200;
// Set reasonable size limits
const maxSize = 400;
const minSize = 100;
let newWidth, newHeight;
// If image is wider than tall (landscape)
if (imageAspect > 1) {
// Use current width as base, calculate height to match aspect ratio
newWidth = Math.min(currentWidth, maxSize);
newHeight = newWidth / imageAspect;
// Ensure height is not too small
if (newHeight < minSize) {
newHeight = minSize;
newWidth = newHeight * imageAspect;
}
} else {
// Image is taller than wide (portrait) - use current height as base
newHeight = Math.min(currentHeight, maxSize);
newWidth = newHeight * imageAspect;
// Ensure width is not too small
if (newWidth < minSize) {
newWidth = minSize;
newHeight = newWidth / imageAspect;
}
}
// Apply the new size that matches the image aspect ratio
mediaItem.style.width = Math.round(newWidth) + 'px';
mediaItem.style.height = Math.round(newHeight) + 'px';
console.log(`[DEBUG] Adjusted container aspect ratio: ${Math.round(newWidth)}x${Math.round(newHeight)} for ${naturalWidth}x${naturalHeight} image`);
// Update stored position data
const x = parseInt(mediaItem.style.left) || 0;
const y = parseInt(mediaItem.style.top) || 0;
this.updateMediaItemPosition(mediaItem, x, y, newWidth, newHeight);
} catch (error) {
console.error('[ERROR] Failed to adjust container aspect ratio:', error);
}
}
// 🎨 ENHANCED LAYERING SYSTEM: Proper z-index management with natural stacking
bringMediaToTop(item) {
console.log('[LAYERING] 🎯 Bringing media item to top with enhanced system');
// Initialize the layering system if not already done
if (!window.mediaLayerManager) {
this.initializeLayerManager();
}
// Use the centralized layer manager
window.mediaLayerManager.bringToFront(item);
}
// 🎨 CENTRALIZED LAYER MANAGEMENT SYSTEM
initializeLayerManager() {
window.mediaLayerManager = {
// Layer ranges - clean, maintainable z-index hierarchy
layers: {
ANNOTATIONS: 5000, // Always on top - annotations/drawing canvas
SELECTED_MEDIA: 1000, // Selected media item (user's choice)
MEDIA_STACK: 100, // Base layer for media stacking
BACKGROUND: 1 // Background elements
},
// Track the stacking order for natural flow
stackingOrder: new Map(), // item -> order number
nextStackOrder: 1,
// 🎯 Bring specific media item to front (user's choice)
bringToFront(item) {
console.log('[LAYER MGR] 🎯 Bringing item to front');
// Give this item the top position in media layer
this.setItemLayer(item, this.layers.SELECTED_MEDIA);
// Update stacking order
this.stackingOrder.set(item, this.nextStackOrder++);
// Ensure other media items are properly layered below
this.updateMediaStacking();
// Always ensure annotations stay on top
this.ensureAnnotationsOnTop();
console.log('[LAYER MGR] ✅ Item brought to front successfully');
this.debugLayers();
},
// 🔄 Update the natural stacking order of all media
updateMediaStacking() {
const allMediaItems = document.querySelectorAll('.canvas-media-item');
const itemsWithOrder = Array.from(allMediaItems).map(item => ({
item,
order: this.stackingOrder.get(item) || 0
}));
// Sort by stacking order (oldest first, newest last)
itemsWithOrder.sort((a, b) => a.order - b.order);
// Apply layered z-indexes
itemsWithOrder.forEach(({item, order}, index) => {
if (order === Math.max(...this.stackingOrder.values())) {
// This is the most recently selected - keep it at SELECTED_MEDIA level
return;
}
// Other items get stacked in order
const zIndex = this.layers.MEDIA_STACK + index;
this.setItemLayer(item, zIndex);
console.log(`[LAYER MGR] Stacked item ${index} at z-index ${zIndex}`);
});
},
// 🎨 Set specific z-index for an item
setItemLayer(item, zIndex) {
item.style.zIndex = zIndex.toString();
item.style.setProperty('z-index', zIndex.toString(), 'important');
// Ensure positioning works
if (!item.style.position || item.style.position === 'static') {
item.style.position = 'absolute';
}
// Force reflow
item.offsetHeight;
},
// 🔒 Ensure annotations always stay on top
ensureAnnotationsOnTop() {
const drawingCanvases = document.querySelectorAll('.media-drawing-canvas');
drawingCanvases.forEach(canvas => {
canvas.style.zIndex = this.layers.ANNOTATIONS.toString();
canvas.style.setProperty('z-index', this.layers.ANNOTATIONS.toString(), 'important');
console.log('[LAYER MGR] 🎨 Annotation canvas secured at z-index', this.layers.ANNOTATIONS);
});
},
// 🐛 Debug current layering state
debugLayers() {
console.log('[LAYER MGR] 📊 Current layer state:');
console.log('Annotations (z-index 5000+):', document.querySelectorAll('.media-drawing-canvas').length);
const mediaItems = document.querySelectorAll('.canvas-media-item');
mediaItems.forEach((item, index) => {
const zIndex = window.getComputedStyle(item).zIndex;
const order = this.stackingOrder.get(item) || 'none';
console.log(`Media ${index + 1}: z-index ${zIndex}, stack order ${order}`);
});
},
// 🔄 Reset layering system (if needed)
reset() {
this.stackingOrder.clear();
this.nextStackOrder = 1;
console.log('[LAYER MGR] 🔄 Layer system reset');
},
// 🎨 Refresh all layering (call during tool switches)
refreshAllLayers() {
console.log('[LAYER MGR] 🔄 Refreshing all layers');
// Ensure annotations stay on top
this.ensureAnnotationsOnTop();
// Update media stacking
this.updateMediaStacking();
console.log('[LAYER MGR] ✅ All layers refreshed');
}
};
console.log('[LAYERING] 🎨 Enhanced layer manager initialized');
}
// Debug function to show current layering state
debugCurrentLayering() {
const allMediaItems = document.querySelectorAll('.canvas-media-item');
console.log('🔍 [LAYERING DEBUG] Current media item z-index values:');
allMediaItems.forEach((item, index) => {
const computedStyle = window.getComputedStyle(item);
const styleZIndex = item.style.zIndex;
const computedZIndex = computedStyle.zIndex;
const position = computedStyle.position;
console.log(` ${index + 1}. style.zIndex: ${styleZIndex}, computed: ${computedZIndex}, position: ${position}`);
});
}
makeMediaItemInteractive(item) {
console.log('[DEBUG] Making media item interactive:', item);
console.log('[DEBUG] Current tool state:', window.currentTool);
console.log('[DEBUG] Select mode active:', window.selectModeActive);
// CRITICAL FIX: Always remove data-drawing-mode first to ensure clean state
item.removeAttribute('data-drawing-mode');
// FORCE REFRESH: Remove any conflicting styles first
item.style.removeProperty('cursor');
item.style.removeProperty('pointer-events');
// Check current tool state to set appropriate pointer events
const isDrawingTool = window.currentTool === 'pen' ||
window.currentTool === 'highlight' ||
window.currentTool === 'erase';
const isSelectTool = window.currentTool === 'select';
// Set pointer events based on current tool
if (isDrawingTool) {
item.style.pointerEvents = 'none';
item.style.setProperty('pointer-events', 'none', 'important');
item.style.cursor = 'default';
item.setAttribute('data-drawing-mode', 'true');
} else if (isSelectTool) {
// When select tool is active, enable pointer events for media manipulation
item.style.pointerEvents = 'auto';
item.style.setProperty('pointer-events', 'auto', 'important');
item.style.cursor = 'move';
item.style.setProperty('cursor', 'move', 'important');
// Ensure data-drawing-mode is definitely removed
item.removeAttribute('data-drawing-mode');
} else {
// Default state - enable pointer events
item.style.pointerEvents = 'auto';
item.style.setProperty('pointer-events', 'auto', 'important');
item.style.cursor = 'grab';
// Ensure data-drawing-mode is removed for default state too
item.removeAttribute('data-drawing-mode');
}
item.style.userSelect = 'none';
item.style.webkitUserSelect = 'none';
// Stable z-index system - media items never change layers
// PRESERVE LAYERING: Set base z-index only if not already higher
const currentZIndex = parseInt(item.style.zIndex) || 100;
if (currentZIndex <= 100) {
item.style.zIndex = '100';
item.style.setProperty('z-index', '100', 'important');
}
console.log(`[LAYERING] makeMediaItemInteractive - preserved z-index: ${item.style.zIndex}`);
console.log('[DEBUG] Item styles after setup:', {
pointerEvents: item.style.pointerEvents,
cursor: item.style.cursor,
zIndex: item.style.zIndex,
hasDrawingMode: item.hasAttribute('data-drawing-mode'),
drawingModeValue: item.getAttribute('data-drawing-mode')
});
// Function to ensure this item is interactive WITHOUT changing layers
const ensureInteractive = () => {
if (window.currentTool === 'select') {
// Ensure pointer events are enabled
item.style.pointerEvents = 'auto';
item.style.setProperty('pointer-events', 'auto', 'important');
// PRESERVE LAYERING: Don't reset z-index if item was brought to front
const currentZIndex = parseInt(item.style.zIndex) || 100;
if (currentZIndex <= 100) {
// Only set to 100 if it's not already at a higher value from bringMediaToTop
item.style.zIndex = '100';
item.style.setProperty('z-index', '100', 'important');
}
console.log(`[LAYERING] ensureInteractive - preserved z-index: ${item.style.zIndex} for item`);
console.log('[DEBUG] Ensured media item is interactive for select tool, maintaining z-index: 100');
}
};
let isDragging = false;
let startX, startY, startLeft, startTop;
let initialPinchDistance = 0;
let initialWidth = 0;
let initialHeight = 0;
const handleTouchStart = (e) => {
// SAFETY: Ensure media item is visible before any interaction
item.style.display = 'block';
item.style.visibility = 'visible';
item.style.opacity = '1';
console.log('[DEBUG] Touch start on media item:', e);
console.log('[DEBUG] Current tool on touch start:', window.currentTool);
console.log('[DEBUG] Touch start - ensured media visibility');
// Check if drawing tools are active - if so, don't handle drag operations
const isDrawingToolActive = window.currentTool === 'pen' ||
window.currentTool === 'highlight' ||
window.currentTool === 'erase';
// Also check if the item is in drawing mode
if (isDrawingToolActive || item.getAttribute('data-drawing-mode') === 'true') {
console.log('[DEBUG] Drawing tool active or item in drawing mode, not handling drag - letting drawing canvas handle it');
// Don't prevent default or stop propagation for drawing tools
// Let the drawing canvas handle the events
return;
}
// For select tool or any other tool, allow media item dragging
console.log('[DEBUG] Tool state allows media dragging:', window.currentTool);
// Ensure this item is interactive when interaction starts
ensureInteractive();
// LAYERING: Bring this media item to top when touched
this.bringMediaToTop(item);
if (e.touches.length === 2) {
// Two finger touch - prepare for pinch/zoom
e.preventDefault();
e.stopPropagation();
// MARK: Flag this as multi-touch to prevent double-tap conflicts
item._hadMultiTouch = true;
item._isInPinchGesture = true;
const touch1 = e.touches[0];
const touch2 = e.touches[1];
initialPinchDistance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
// Store initial dimensions and position for center-point scaling
initialWidth = item.offsetWidth;
initialHeight = item.offsetHeight;
item._initialLeft = parseInt(item.style.left) || 0;
item._initialTop = parseInt(item.style.top) || 0;
isDragging = false;
console.log('[DEBUG] Started pinch gesture, initial size:', initialWidth, 'x', initialHeight);
console.log('[DEBUG] Marked as multi-touch to prevent double-tap conflicts');
} else if (e.touches.length === 1) {
// Single finger touch - prepare for drag
e.preventDefault();
e.stopPropagation();
// CLEAR: Reset multi-touch flags for single touch (allows double-tap)
item._hadMultiTouch = false;
item._isInPinchGesture = false;
isDragging = true;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startLeft = parseInt(item.style.left) || 0;
startTop = parseInt(item.style.top) || 0;
item.style.cursor = 'grabbing';
console.log('[DEBUG] Started dragging media item - single touch, double-tap enabled');
}
};
const handleTouchMove = (e) => {
// Check if drawing tools are active - if so, don't handle move operations
const isDrawingToolActive = window.currentTool === 'pen' ||
window.currentTool === 'highlight' ||
window.currentTool === 'erase';
if (isDrawingToolActive) {
console.log('[DEBUG] Drawing tool active, not handling move - letting drawing canvas handle it');
// Don't prevent default or stop propagation for drawing tools
return;
}
// Throttle touch move events for better performance
const now = Date.now();
if (item._lastTouchMoveTime && now - item._lastTouchMoveTime < 16) { // ~60fps
return;
}
item._lastTouchMoveTime = now;
// For select tool or any other tool, allow media item dragging
console.log('[DEBUG] Tool state allows media move:', window.currentTool);
if (e.touches.length === 2 && initialPinchDistance > 0) {
// Handle pinch/zoom with smooth center-point scaling
e.preventDefault();
e.stopPropagation();
// MARK: Ensure multi-touch flag is set during pinch
item._hadMultiTouch = true;
item._isInPinchGesture = true;
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
const scale = currentDistance / initialPinchDistance;
const newWidth = Math.max(150, Math.min(1500, initialWidth * scale));
const newHeight = Math.max(100, Math.min(1000, initialHeight * scale));
// IMPROVED: Use stored initial position for center point calculation
const centerX = item._initialLeft + initialWidth / 2;
const centerY = item._initialTop + initialHeight / 2;
// Apply new size smoothly - ALWAYS ensure item stays visible
item.style.width = `${newWidth}px`;
item.style.height = `${newHeight}px`;
item.style.display = 'block'; // FORCE visibility during resize
item.style.visibility = 'visible'; // FORCE visibility during resize
// Maintain center point during resize
const newLeft = centerX - newWidth / 2;
const newTop = centerY - newHeight / 2;
item.style.left = `${newLeft}px`;
item.style.top = `${newTop}px`;
// Store current position for touchend (don't call updateMediaItemPosition yet)
item._currentLeft = newLeft;
item._currentTop = newTop;
item._currentWidth = newWidth;
item._currentHeight = newHeight;
// THROTTLED: Only update annotations occasionally during resize for performance
if (!item._annotationUpdatePending) {
item._annotationUpdatePending = true;
// Use requestAnimationFrame to prevent blocking touch events
requestAnimationFrame(() => {
this.updateAnnotationsForMovedMediaItem(item);
item._annotationUpdatePending = false;
});
}
console.log('[DEBUG] Touch resize:', { newWidth, newHeight, newLeft, newTop });
} else if (isDragging && e.touches.length === 1) {
// Handle single-touch dragging
e.preventDefault();
e.stopPropagation();
const deltaX = e.touches[0].clientX - startX;
const deltaY = e.touches[0].clientY - startY;
const newLeft = startLeft + deltaX;
const newTop = startTop + deltaY;
item.style.left = `${newLeft}px`;
item.style.top = `${newTop}px`;
// Update stored data
this.updateMediaItemPosition(item, newLeft, newTop, item.offsetWidth, item.offsetHeight);
// Update annotations during drag
this.updateAnnotationsForMovedMediaItem(item);
}
};
const handleTouchEnd = () => {
console.log('[DEBUG] Touch end on media item');
// CRITICAL: Mark end of pinch gesture to prevent double-tap conflicts
if (item._isInPinchGesture) {
item._lastPinchTime = Date.now();
item._isInPinchGesture = false;
console.log('[DEBUG] Pinch gesture ended - marked timestamp to prevent double-tap');
}
// CRITICAL: Use stored current position from touch move, or fallback to computed values
const finalLeft = item._currentLeft !== undefined ? item._currentLeft : (parseInt(item.style.left) || 0);
const finalTop = item._currentTop !== undefined ? item._currentTop : (parseInt(item.style.top) || 0);
const finalWidth = item._currentWidth !== undefined ? item._currentWidth : item.offsetWidth;
const finalHeight = item._currentHeight !== undefined ? item._currentHeight : item.offsetHeight;
// FORCE item to be visible and positioned correctly
item.style.display = 'block';
item.style.visibility = 'visible';
item.style.opacity = '1';
item.style.left = `${finalLeft}px`;
item.style.top = `${finalTop}px`;
item.style.width = `${finalWidth}px`;
item.style.height = `${finalHeight}px`;
// Update stored data with final position/size
this.updateMediaItemPosition(item, finalLeft, finalTop, finalWidth, finalHeight);
console.log('[DEBUG] Saved final touch position:', { finalLeft, finalTop, finalWidth, finalHeight });
// Final annotation update
this.updateAnnotationsForMovedMediaItem(item);
isDragging = false;
initialPinchDistance = 0;
item.style.cursor = 'grab';
// Clear throttling timers and stored positions
item._lastTouchMoveTime = 0;
item._lastWheelTime = 0;
item._currentLeft = undefined;
item._currentTop = undefined;
item._currentWidth = undefined;
item._currentHeight = undefined;
item._annotationUpdatePending = false;
// Keep the active media on top after interaction ends
// Don't reset z-index - let the active media stay in front
console.log('[DEBUG] Touch end complete - media should be visible');
};
// Mouse events for dragging and resizing
const handleMouseDown = (e) => {
console.log('[DEBUG] Mouse down on media item:', e);
console.log('[DEBUG] Current tool:', window.currentTool);
// Check if drawing tools are active - if so, don't handle drag operations
const isDrawingToolActive = window.currentTool === 'pen' ||
window.currentTool === 'highlight' ||
window.currentTool === 'erase';
// Also check if the item is in drawing mode
if (isDrawingToolActive || item.getAttribute('data-drawing-mode') === 'true') {
console.log('[DEBUG] Drawing tool active or item in drawing mode, not handling mouse down - letting drawing canvas handle it');
console.log('[DEBUG] isDrawingToolActive:', isDrawingToolActive, 'data-drawing-mode:', item.getAttribute('data-drawing-mode'));
// Don't prevent default or stop propagation for drawing tools
// Let the drawing canvas handle the events
return;
}
// Only handle interactions for select tool
if (window.currentTool !== 'select') {
return;
}
console.log('[DEBUG] Select tool allows media interaction');
// Ensure this item is interactive when interaction starts
ensureInteractive();
// LAYERING: Bring this media item to top when clicked
this.bringMediaToTop(item);
e.preventDefault();
e.stopPropagation();
// Check if this is a resize operation (near edges)
const rect = item.getBoundingClientRect();
const margin = 10; // pixels from edge to trigger resize
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
let resizeMode = '';
if (x <= margin && y <= margin) resizeMode = 'nw';
else if (x >= rect.width - margin && y <= margin) resizeMode = 'ne';
else if (x <= margin && y >= rect.height - margin) resizeMode = 'sw';
else if (x >= rect.width - margin && y >= rect.height - margin) resizeMode = 'se';
else if (x <= margin) resizeMode = 'w';
else if (x >= rect.width - margin) resizeMode = 'e';
else if (y <= margin) resizeMode = 'n';
else if (y >= rect.height - margin) resizeMode = 's';
if (resizeMode) {
// Start resize operation
item._resizing = true;
item._resizeMode = resizeMode;
item._startWidth = item.offsetWidth;
item._startHeight = item.offsetHeight;
item._startLeft = parseInt(item.style.left) || 0;
item._startTop = parseInt(item.style.top) || 0;
item.style.cursor = resizeMode + '-resize';
console.log('[DEBUG] Starting resize operation:', resizeMode);
} else {
// Start drag operation
isDragging = true;
item.style.cursor = 'move';
}
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(item.style.left) || 0;
startTop = parseInt(item.style.top) || 0;
};
const handleMouseMove = (e) => {
// Check if drawing tools are active - if so, don't handle move operations
const isDrawingToolActive = window.currentTool === 'pen' ||
window.currentTool === 'highlight' ||
window.currentTool === 'erase';
// Also check if the item is in drawing mode
if (isDrawingToolActive || item.getAttribute('data-drawing-mode') === 'true') {
return;
}
// Only handle for select tool
if (window.currentTool !== 'select') {
return;
}
// Throttle mouse move events for better performance
const now = Date.now();
if (item._lastMouseMoveTime && now - item._lastMouseMoveTime < 16) { // ~60fps
return;
}
item._lastMouseMoveTime = now;
// Update cursor based on position if not dragging/resizing
if (!isDragging && !item._resizing) {
const rect = item.getBoundingClientRect();
const margin = 10;
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
let cursor = 'move';
if (x <= margin && y <= margin) cursor = 'nw-resize';
else if (x >= rect.width - margin && y <= margin) cursor = 'ne-resize';
else if (x <= margin && y >= rect.height - margin) cursor = 'sw-resize';
else if (x >= rect.width - margin && y >= rect.height - margin) cursor = 'se-resize';
else if (x <= margin) cursor = 'w-resize';
else if (x >= rect.width - margin) cursor = 'e-resize';
else if (y <= margin) cursor = 'n-resize';
else if (y >= rect.height - margin) cursor = 's-resize';
item.style.cursor = cursor;
return;
}
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
if (item._resizing) {
// IMPROVED: Handle resize operation with center-point option for corners
const mode = item._resizeMode;
// For corner resizes, maintain center point like touch resize
if (mode === 'se' || mode === 'nw' || mode === 'ne' || mode === 'sw') {
// Center-point resize for corner drags
const scale = 1 + (deltaX + deltaY) / (item._startWidth + item._startHeight);
const newWidth = Math.max(150, Math.min(1500, item._startWidth * scale));
const newHeight = Math.max(100, Math.min(1000, item._startHeight * scale));
// Calculate center point
const centerX = item._startLeft + item._startWidth / 2;
const centerY = item._startTop + item._startHeight / 2;
// Apply new size maintaining center
const newLeft = centerX - newWidth / 2;
const newTop = centerY - newHeight / 2;
item.style.width = `${newWidth}px`;
item.style.height = `${newHeight}px`;
item.style.left = `${newLeft}px`;
item.style.top = `${newTop}px`;
// Update stored data
this.updateMediaItemPosition(item, newLeft, newTop, newWidth, newHeight);
} else {
// Edge resize - traditional behavior
let newWidth = item._startWidth;
let newHeight = item._startHeight;
let newLeft = item._startLeft;
let newTop = item._startTop;
if (mode.includes('e')) newWidth = Math.max(150, item._startWidth + deltaX);
if (mode.includes('w')) {
newWidth = Math.max(150, item._startWidth - deltaX);
newLeft = item._startLeft + (item._startWidth - newWidth);
}
if (mode.includes('s')) newHeight = Math.max(100, item._startHeight + deltaY);
if (mode.includes('n')) {
newHeight = Math.max(100, item._startHeight - deltaY);
newTop = item._startTop + (item._startHeight - newHeight);
}
item.style.width = `${newWidth}px`;
item.style.height = `${newHeight}px`;
item.style.left = `${newLeft}px`;
item.style.top = `${newTop}px`;
// Update stored data
this.updateMediaItemPosition(item, newLeft, newTop, newWidth, newHeight);
}
// SEAMLESS INTEGRATION: Update annotations during resize
this.updateAnnotationsForMovedMediaItem(item);
} else if (isDragging) {
// Handle drag operation
const newLeft = startLeft + deltaX;
const newTop = startTop + deltaY;
item.style.left = `${newLeft}px`;
item.style.top = `${newTop}px`;
// Update stored data
this.updateMediaItemPosition(item, newLeft, newTop, item.offsetWidth, item.offsetHeight);
// SEAMLESS INTEGRATION: Update annotations in real-time during drag
this.updateAnnotationsForMovedMediaItem(item);
}
};
const handleMouseUp = () => {
console.log('[DEBUG] Mouse up on media item');
isDragging = false;
item._resizing = false;
item._resizeMode = '';
// Clear throttling timers
item._lastMouseMoveTime = 0;
item._lastWheelTime = 0;
// Keep the active media on top after interaction ends
// Don't reset z-index - let the active media stay in front
if (window.currentTool === 'select') {
item.style.cursor = 'move';
} else {
item.style.cursor = 'grab';
}
};
// Add touch event listeners with capture to prevent event bubbling
item.addEventListener('touchstart', handleTouchStart, { passive: false, capture: true });
item.addEventListener('touchmove', handleTouchMove, { passive: false, capture: true });
item.addEventListener('touchend', handleTouchEnd, { capture: true });
// Simple click handler to bring media to front on any click
const handleClick = (e) => {
console.log('[DEBUG] Click on media item - bringing to front');
// Only respond to clicks when select tool is active
if (window.currentTool === 'select') {
// Bring this media item to the front
this.bringMediaToTop(item);
// Prevent event from bubbling
e.stopPropagation();
}
};
// Mouse wheel handler for smooth resizing
const handleWheel = (e) => {
// Only handle wheel events for select tool
if (window.currentTool !== 'select') {
return;
}
e.preventDefault();
e.stopPropagation();
// LAYERING: Bring this media item to top when resizing with wheel
this.bringMediaToTop(item);
// SMOOTH: Throttle wheel events for smooth resizing like touch
const now = Date.now();
if (item._lastWheelTime && now - item._lastWheelTime < 16) { // ~60fps like touch
return;
}
item._lastWheelTime = now;
// SMOOTH: Smaller delta increments for smoother scaling
const delta = e.deltaY > 0 ? 0.95 : 1.05; // Smaller steps for smoother effect
const currentWidth = item.offsetWidth;
const currentHeight = item.offsetHeight;
const newWidth = Math.max(150, Math.min(1500, currentWidth * delta));
const newHeight = Math.max(100, Math.min(1000, currentHeight * delta));
// Calculate the center point of the item before scaling
const currentLeft = parseInt(item.style.left) || 0;
const currentTop = parseInt(item.style.top) || 0;
const centerX = currentLeft + currentWidth / 2;
const centerY = currentTop + currentHeight / 2;
// Apply new size smoothly
item.style.width = `${newWidth}px`;
item.style.height = `${newHeight}px`;
// Adjust position to keep the center point the same
const newLeft = centerX - newWidth / 2;
const newTop = centerY - newHeight / 2;
item.style.left = `${newLeft}px`;
item.style.top = `${newTop}px`;
// SMOOTH: Throttled updates for better performance
if (!item._wheelUpdatePending) {
item._wheelUpdatePending = true;
// Batch updates using requestAnimationFrame for smooth rendering
requestAnimationFrame(() => {
// Update stored data
this.updateMediaItemPosition(item, newLeft, newTop, newWidth, newHeight);
// Update annotations
this.updateAnnotationsForMovedMediaItem(item);
// Clear throttling flag
item._wheelUpdatePending = false;
});
}
};
// Remove any existing event listeners to prevent duplicates
if (item._mouseDownHandler) {
item.removeEventListener('mousedown', item._mouseDownHandler);
}
if (item._mouseMoveHandler) {
document.removeEventListener('mousemove', item._mouseMoveHandler);
}
if (item._mouseUpHandler) {
document.removeEventListener('mouseup', item._mouseUpHandler);
}
if (item._clickHandler) {
item.removeEventListener('click', item._clickHandler);
}
if (item._wheelHandler) {
item.removeEventListener('wheel', item._wheelHandler);
}
// Store handlers for potential removal
item._mouseDownHandler = handleMouseDown;
item._mouseMoveHandler = handleMouseMove;
item._mouseUpHandler = handleMouseUp;
item._clickHandler = handleClick;
item._wheelHandler = handleWheel;
// Add mouse event listeners with proper options
item.addEventListener('mousedown', handleMouseDown, { passive: false });
document.addEventListener('mousemove', handleMouseMove, { passive: false });
document.addEventListener('mouseup', handleMouseUp, { passive: false });
// Add wheel event listener for scroll-to-resize
item.addEventListener('wheel', handleWheel, { passive: false });
// Add click listener to bring media to front on simple clicks
item.addEventListener('click', handleClick, { capture: true });
console.log('[DEBUG] Added mouse, wheel, and click event listeners to media item, current tool:', window.currentTool);
// Set appropriate cursor based on current tool
const updateCursor = () => {
if (window.currentTool === 'select') {
item.style.setProperty('cursor', 'move', 'important');
console.log('[DEBUG] Set cursor to move for select tool on item');
} else {
item.style.setProperty('cursor', 'grab', 'important');
}
};
updateCursor();
// Update cursor on mouse enter/leave for select tool
item.addEventListener('mouseenter', (e) => {
updateCursor();
console.log('[DEBUG] Mouse enter on media item, cursor should be:', window.currentTool === 'select' ? 'move' : 'grab');
});
item.addEventListener('mouseleave', () => {
updateCursor();
});
// Double-tap/click detection for erasing media items
let lastTapTime = 0;
let tapCount = 0;
const doubleTapDelay = 300; // milliseconds
const handleDoubleTap = (e) => {
console.log('[DEBUG] Double tap/click detected on media item');
e.preventDefault();
e.stopPropagation();
// Remove the media item
this.removeMediaItemFromTab(item);
item.remove();
console.log('[DEBUG] Media item removed by double tap/click');
};
const handleSingleTap = (e) => {
// CRITICAL: Prevent double-tap deletion during pinch-to-zoom gestures
// Check if this is a touch event from pinch gesture
if (e.type === 'touchend') {
// Ignore multi-touch events (pinch gestures)
if (e.touches && e.touches.length > 0) {
console.log('[DEBUG] Ignoring tap - multi-touch still active');
return;
}
// Ignore if pinch was recently active (within last 500ms)
if (item._lastPinchTime && Date.now() - item._lastPinchTime < 500) {
console.log('[DEBUG] Ignoring tap - recent pinch gesture detected');
return;
}
// Ignore if this touch event had multiple touches during the gesture
if (item._hadMultiTouch) {
console.log('[DEBUG] Ignoring tap - was part of multi-touch gesture');
item._hadMultiTouch = false; // Reset flag
return;
}
}
const currentTime = Date.now();
const timeDiff = currentTime - lastTapTime;
if (timeDiff < doubleTapDelay) {
// Double tap detected
tapCount = 0;
lastTapTime = 0;
console.log('[DEBUG] Double tap confirmed - removing media');
handleDoubleTap(e);
} else {
// Single tap - start timer
tapCount = 1;
lastTapTime = currentTime;
console.log('[DEBUG] Single tap detected - starting double-tap timer');
// Reset tap count after delay
setTimeout(() => {
if (tapCount === 1) {
tapCount = 0;
lastTapTime = 0;
console.log('[DEBUG] Double-tap timer expired - single tap confirmed');
}
}, doubleTapDelay);
}
};
// Add eraser tool functionality to delete media items
const handleEraserClick = (e) => {
console.log('[DEBUG] Eraser click handler called, currentTool:', window.currentTool);
// Check if eraser tool is active (with fallback)
const isEraserActive = window.currentTool === 'erase' ||
(window.currentTool === undefined &&
document.querySelector('.tool-icon[data-tool="erase"]')?.classList.contains('active'));
if (isEraserActive) {
e.preventDefault();
e.stopPropagation();
console.log('[DEBUG] Eraser tool deleting media item');
// Remove from tab data before removing from DOM
this.removeMediaItemFromTab(item);
item.remove();
return true; // Indicate that the event was handled
}
console.log('[DEBUG] Eraser tool not active, not deleting');
return false; // Event was not handled
};
// Add click/tap listeners for eraser deletion with higher priority
item.addEventListener('click', handleEraserClick, { capture: true });
item.addEventListener('touchend', handleEraserClick, { capture: true });
// Add double-tap/click listeners for media item deletion
item.addEventListener('click', handleSingleTap);
item.addEventListener('touchend', handleSingleTap);
}
getFileType(extension) {
const typeMap = {
jpg: 'image',
jpeg: 'image',
png: 'image',
gif: 'image',
mp4: 'video',
mp3: 'audio',
pdf: 'pdf'
};
return typeMap[extension.toLowerCase()] || 'file';
}
getFileIcon(fileType) {
const icons = {
image: '🖼️',
video: '🎥',
audio: '🎵',
pdf: '📄',
file: '📁'
};
return icons[fileType] || '📁';
}
// Save media item to tab data for persistence
saveMediaItemToTab(mediaData, x, y, width, height) {
console.log('[DEBUG] saveMediaItemToTab called:', { mediaData, x, y, width, height });
if (!window.bibleApp || !window.bibleApp.activeTabId) {
console.error('[ERROR] No active tab to save media item to');
return;
}
const tab = window.bibleApp.tabs.find(t => t.id === window.bibleApp.activeTabId);
if (!tab) {
console.error('[ERROR] Active tab not found');
return;
}
// Initialize mediaItems array if it doesn't exist
if (!tab.mediaItems) {
tab.mediaItems = [];
console.log('[DEBUG] Initialized mediaItems array for tab:', tab.id);
}
// Create media item data
const mediaItemData = {
id: Date.now() + Math.random(), // Unique ID
path: mediaData.path,
name: mediaData.name,
type: mediaData.type,
x: parseInt(x),
y: parseInt(y),
width: parseInt(width),
height: parseInt(height)
};
// Add to tab's media items
tab.mediaItems.push(mediaItemData);
console.log('[DEBUG] Saved media item to tab:', mediaItemData);
console.log('[DEBUG] Tab now has', tab.mediaItems.length, 'media items');
console.log('[DEBUG] All media items in tab:', tab.mediaItems);
return mediaItemData;
}
// Remove media item from tab data when deleted
removeMediaItemFromTab(mediaItem) {
if (!window.bibleApp || !window.bibleApp.activeTabId) {
console.error('[ERROR] No active tab to remove media item from');
return;
}
const tab = window.bibleApp.tabs.find(t => t.id === window.bibleApp.activeTabId);
if (!tab || !tab.mediaItems) {
console.error('[ERROR] Active tab or mediaItems not found');
return;
}
const mediaId = mediaItem.dataset.mediaId;
const mediaPath = mediaItem.dataset.mediaPath;
const mediaType = mediaItem.dataset.mediaType;
// Find by unique id when available (the same file can be added multiple times with the
// same path/type); fall back to path+type for items created before an id was stamped.
let index = -1;
if (mediaId != null && mediaId !== '') {
index = tab.mediaItems.findIndex(item => String(item.id) === String(mediaId));
}
if (index === -1) {
index = tab.mediaItems.findIndex(item => item.path === mediaPath && item.type === mediaType);
}
if (index !== -1) {
tab.mediaItems.splice(index, 1);
console.log('[DEBUG] Removed media item from tab data, remaining:', tab.mediaItems.length);
}
// Remove associated strokes
this.removeStrokesForMediaItem(window.bibleApp.activeTabId, mediaPath);
}
removeStrokesForMediaItem(tabId, mediaPath) {
if (!window.bibleApp) return;
const tab = window.bibleApp.tabs.find(t => t.id === tabId);
if (!tab || !tab.mediaStrokes) return;
// Remove strokes associated with this media item
const originalLength = tab.mediaStrokes.length;
tab.mediaStrokes = tab.mediaStrokes.filter(stroke => stroke.mediaItemId !== mediaPath);
const removedCount = originalLength - tab.mediaStrokes.length;
if (removedCount > 0) {
console.log('[DEBUG] Removed', removedCount, 'strokes associated with media item:', mediaPath);
// Redraw the canvas to reflect the changes
this.redrawAllStrokes(tabId);
}
}
redrawAllStrokes(tabId) {
if (!window.bibleApp) return;
const tab = window.bibleApp.tabs.find(t => t.id === tabId);
if (!tab || !tab.mediaStrokes || tab.mediaStrokes.length === 0) return;
const canvas = document.getElementById(`mediaDrawingCanvas${tabId}`);
if (!canvas) return;
const ctx = canvas.getContext('2d');
// Clear the canvas first
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Redraw all strokes
tab.mediaStrokes.forEach(strokeData => {
const settings = strokeData.settings;
ctx.globalCompositeOperation = settings.composite;
ctx.strokeStyle = settings.color;
ctx.lineWidth = settings.size;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalAlpha = settings.alpha;
// Transform coordinates if this stroke is associated with a media item
let pointsToDraw = strokeData.points;
if (strokeData.mediaItemId) {
const mediaItem = document.querySelector(`[data-media-path="${strokeData.mediaItemId}"]`);
if (mediaItem) {
const transformedStroke = this.transformStrokeFromMediaItem(strokeData, mediaItem);
pointsToDraw = transformedStroke.points;
}
}
if (pointsToDraw.length > 1) {
ctx.beginPath();
ctx.moveTo(pointsToDraw[0].x, pointsToDraw[0].y);
for (let i = 1; i < pointsToDraw.length; i++) {
ctx.lineTo(pointsToDraw[i].x, pointsToDraw[i].y);
}
ctx.stroke();
}
});
console.log('[DEBUG] Redrew all strokes for tab:', tabId);
}
// Remove media thumbnail from tab data when deleted
removeMediaThumbnailFromTab(mediaPath, mediaType) {
if (!window.bibleApp || !window.bibleApp.activeTabId) {
console.error('[ERROR] No active tab to remove media thumbnail from');
return;
}
const tab = window.bibleApp.tabs.find(t => t.id === window.bibleApp.activeTabId);
if (!tab || !tab.mediaThumbnails) {
console.error('[ERROR] Active tab or mediaThumbnails not found');
return;
}
// Find and remove the media thumbnail from tab data
const index = tab.mediaThumbnails.findIndex(thumbnail =>
thumbnail.path === mediaPath && thumbnail.type === mediaType
);
if (index !== -1) {
tab.mediaThumbnails.splice(index, 1);
console.log('[DEBUG] Removed media thumbnail from tab data, remaining:', tab.mediaThumbnails.length);
}
}
// Restore media items for a tab
restoreMediaItems(tabId) {
console.log('[DEBUG] restoreMediaItems called for tabId:', tabId);
if (!window.bibleApp) {
console.error('[ERROR] BibleApp not found');
return;
}
const tab = window.bibleApp.tabs.find(t => t.id === tabId);
if (!tab) {
console.error('[ERROR] Tab not found:', tabId);
return;
}
if (!tab.mediaItems || tab.mediaItems.length === 0) {
console.log('[DEBUG] No media items to restore for tab:', tabId);
return;
}
console.log('[DEBUG] Restoring', tab.mediaItems.length, 'media items for tab:', tabId);
console.log('[DEBUG] Media items data:', tab.mediaItems);
// Find the media canvas
const mediaCanvas = document.getElementById('mediaCanvas');
if (!mediaCanvas) {
console.error('[ERROR] Media canvas not found');
console.log('[DEBUG] Available elements with "media" in ID:',
Array.from(document.querySelectorAll('[id*="media"]')).map(el => el.id));
return;
}
// Find the canvas content
const canvasContent = mediaCanvas.querySelector('.canvas-content');
if (!canvasContent) {
console.error('[ERROR] Canvas content not found');
console.log('[DEBUG] Media canvas children:',
Array.from(mediaCanvas.children).map(el => el.className));
return;
}
// Placeholder removed per user request - no longer needed
// Restore each media item
tab.mediaItems.forEach((mediaItemData, index) => {
console.log('[DEBUG] Restoring media item', index + 1, 'of', tab.mediaItems.length, ':', mediaItemData);
// Create media item
const mediaItem = document.createElement('div');
mediaItem.className = 'canvas-media-item';
mediaItem.dataset.mediaPath = mediaItemData.path;
mediaItem.dataset.mediaType = mediaItemData.type;
if (mediaItemData.id != null) mediaItem.dataset.mediaId = String(mediaItemData.id);
mediaItem.style.left = mediaItemData.x + 'px';
mediaItem.style.top = mediaItemData.y + 'px';
mediaItem.style.width = mediaItemData.width + 'px';
mediaItem.style.height = mediaItemData.height + 'px';
// Build via DOM APIs (never innerHTML) to prevent stored XSS through file name/path.
if (mediaItemData.type === 'image') {
const img = document.createElement('img');
img.src = mediaItemData.path;
img.alt = mediaItemData.name || '';
img.style.cssText = 'width:100%; height:100%; object-fit:contain;';
mediaItem.appendChild(img);
} else {
const iframe = document.createElement('iframe');
iframe.src = mediaItemData.path;
iframe.setAttribute('frameborder', '0');
iframe.style.cssText = 'width:100%; height:100%;';
mediaItem.appendChild(iframe);
}
// Ensure restored media has proper content sizing without invisible borders
if (mediaItemData.type === 'image') {
const img = mediaItem.querySelector('img');
if (img) {
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'contain'; // Show full image without cropping
img.style.margin = '0';
img.style.padding = '0';
img.style.border = 'none';
img.style.display = 'block';
// IMPORTANT: Do not call adjustContainerAspectRatio() during restore.
// We want to respect the persisted width/height that we already applied above.
}
} else {
const iframe = mediaItem.querySelector('iframe');
if (iframe) {
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.margin = '0';
iframe.style.padding = '0';
iframe.style.border = 'none';
iframe.style.display = 'block';
}
}
// Add to canvas content
canvasContent.appendChild(mediaItem);
this.makeMediaItemInteractive(mediaItem);
console.log('[DEBUG] Successfully restored media item:', mediaItemData.name);
});
console.log('[DEBUG] Media restoration complete for tab:', tabId);
}
restoreMediaThumbnails(tabId) {
console.log('[DEBUG] restoreMediaThumbnails called for tabId:', tabId);
if (!window.bibleApp) {
console.error('[ERROR] BibleApp not found');
return;
}
const tab = window.bibleApp.tabs.find(t => t.id === tabId);
if (!tab) {
console.error('[ERROR] Tab not found:', tabId);
return;
}
if (!tab.mediaThumbnails || tab.mediaThumbnails.length === 0) {
console.log('[DEBUG] No media thumbnails to restore for tab:', tabId);
return;
}
console.log('[DEBUG] Restoring', tab.mediaThumbnails.length, 'media thumbnails for tab:', tabId);
// Find the media scroll container
const container = document.querySelector('.media-scroll-container');
if (!container) {
console.error('[ERROR] Media scroll container not found');
return;
}
// Clear existing thumbnails (except the add button)
const existingCards = container.querySelectorAll('.media-card:not(.add-files)');
existingCards.forEach(card => card.remove());
// Restore each media thumbnail
tab.mediaThumbnails.forEach((thumbnailData, index) => {
console.log('[DEBUG] Restoring media thumbnail', index + 1, 'of', tab.mediaThumbnails.length, ':', thumbnailData);
// Create media card
const card = document.createElement('div');
card.className = 'media-card';
card.dataset.mediaPath = thumbnailData.path;
card.dataset.mediaType = thumbnailData.type;
card.title = thumbnailData.name;
// Create thumbnail
const thumbnail = document.createElement('div');
thumbnail.className = 'media-thumbnail';
if (thumbnailData.type === 'image') {
const img = document.createElement('img');
img.src = thumbnailData.path;
img.alt = thumbnailData.name;
thumbnail.appendChild(img);
} else {
const icon = document.createElement('div');
icon.className = 'file-icon';
icon.textContent = thumbnailData.icon;
thumbnail.appendChild(icon);
}
card.appendChild(thumbnail);
container.appendChild(card);
// Setup drag listeners for the restored card
this.setupDragListeners(card);
console.log('[DEBUG] Drag listeners set up for restored card:', thumbnailData.name);
});
console.log('[DEBUG] Media thumbnails restoration complete for tab:', tabId);
}
// Update media item position and size in stored data
updateMediaItemPosition(mediaItem, x, y, width, height) {
if (!window.bibleApp || !window.bibleApp.activeTabId) {
return;
}
const tab = window.bibleApp.tabs.find(t => t.id === window.bibleApp.activeTabId);
if (!tab || !tab.mediaItems) {
return;
}
const mediaId = mediaItem.dataset.mediaId;
const mediaPath = mediaItem.dataset.mediaPath;
const mediaType = mediaItem.dataset.mediaType;
// Match by unique id when available (duplicates share path/type); fall back to path+type.
let mediaItemData = null;
if (mediaId != null && mediaId !== '') {
mediaItemData = tab.mediaItems.find(item => String(item.id) === String(mediaId));
}
if (!mediaItemData) {
mediaItemData = tab.mediaItems.find(item => item.path === mediaPath && item.type === mediaType);
}
if (mediaItemData) {
mediaItemData.x = parseInt(x);
mediaItemData.y = parseInt(y);
mediaItemData.width = parseInt(width);
mediaItemData.height = parseInt(height);
console.log('[DEBUG] Updated media item position:', mediaItemData);
}
// Redraw strokes associated with this media item
this.redrawStrokesForMediaItem(window.bibleApp.activeTabId, mediaItem);
}
redrawStrokesForMediaItem(tabId, mediaItem) {
if (!window.bibleApp) return;
const tab = window.bibleApp.tabs.find(t => t.id === tabId);
if (!tab || !tab.mediaStrokes || tab.mediaStrokes.length === 0) return;
const canvas = document.getElementById(`mediaDrawingCanvas${tabId}`);
if (!canvas) return;
const ctx = canvas.getContext('2d');
const mediaPath = mediaItem.dataset.mediaPath;
// Clear the canvas first
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Redraw all strokes, transforming those associated with this media item
tab.mediaStrokes.forEach(strokeData => {
const settings = strokeData.settings;
ctx.globalCompositeOperation = settings.composite;
ctx.strokeStyle = settings.color;
ctx.lineWidth = settings.size;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalAlpha = settings.alpha;
// Transform coordinates if this stroke is associated with a media item
let pointsToDraw = strokeData.points;
if (strokeData.mediaItemId) {
const associatedMediaItem = document.querySelector(`[data-media-path="${strokeData.mediaItemId}"]`);
if (associatedMediaItem) {
const transformedStroke = this.transformStrokeFromMediaItem(strokeData, associatedMediaItem);
pointsToDraw = transformedStroke.points;
}
}
if (pointsToDraw.length > 1) {
ctx.beginPath();
ctx.moveTo(pointsToDraw[0].x, pointsToDraw[0].y);
for (let i = 1; i < pointsToDraw.length; i++) {
ctx.lineTo(pointsToDraw[i].x, pointsToDraw[i].y);
}
ctx.stroke();
}
});
console.log('[DEBUG] Redrew strokes for media item:', mediaPath);
}
ensureDrawingCanvasExists(tabId) {
const mediaCanvas = document.getElementById('mediaCanvas');
if (!mediaCanvas) {
console.error('[ERROR] Media canvas not found for drawing canvas setup');
return false;
}
let drawingCanvas = mediaCanvas.querySelector('.media-drawing-canvas');
if (!drawingCanvas) {
console.log('[DEBUG] Drawing canvas not found, creating it');
this.setupMediaDrawingCanvas(tabId);
// Add small timeout to ensure canvas is fully appended to DOM
setTimeout(() => {
drawingCanvas = mediaCanvas.querySelector('.media-drawing-canvas');
if (drawingCanvas) {
// REMOVED: this.updateCanvasStyling(drawingCanvas) - app.js now controls this
this.resizeMediaDrawingCanvas(tabId);
console.log('[DEBUG] Drawing canvas ensured and ready (delayed)');
}
}, 10);
} else {
// Canvas already exists
// REMOVED: this.updateCanvasStyling(drawingCanvas) - app.js now controls this
this.resizeMediaDrawingCanvas(tabId);
console.log('[DEBUG] Drawing canvas ensured and ready');
return true;
}
console.error('[ERROR] Failed to create drawing canvas');
return false;
}
// TEST FUNCTION: Verify touch optimization improvements
testTouchOptimizations() {
console.log('=== TOUCH OPTIMIZATION TEST ===');
const mediaItems = document.querySelectorAll('.canvas-media-item');
console.log('Media items found:', mediaItems.length);
if (mediaItems.length === 0) {
console.log('❌ No media items found. Add media to test touch optimizations.');
return;
}
console.log('✅ Touch optimizations active:');
console.log(' • 60fps throttling on touch/mouse move events');
console.log(' • Center-point resizing for pinch-to-zoom');
console.log(' • Corner mouse resize maintains center point');
console.log(' • SMOOTH mouse wheel resizing with throttling');
console.log(' • Final position saved when touch ends (prevents disappearing)');
console.log(' • Annotations preserved during all resize operations');
console.log(' • Smooth position updates during touch interactions');
console.log(' • CONFLICT RESOLUTION: Pinch gestures won\'t trigger double-tap deletion');
console.log(' • MEDIA LAYERING: User-controlled persistent layering - only changes when YOU click a different media');
console.log(' • PRECISE BOUNDARIES: Container aspect ratios match images - no empty space, no cropping');
console.log('📱 Touch test instructions:');
console.log(' 1. Single touch: Drag to move media items');
console.log(' 2. Pinch gesture: Resize with center point maintained');
console.log(' 3. Mouse corners: Drag corners for center-point resize');
console.log(' 4. Mouse edges: Drag edges for traditional resize');
console.log(' 5. Mouse wheel: Scroll to resize with center point');
console.log(' 6. debugMediaBoundaries() - visualize clickable areas');
console.log(' 7. testAspectRatioFix() - test and fix aspect ratio mismatches');
console.log(' 8. fixMediaBoundaries() - fix any invisible border issues');
console.log(' 9. testMediaLayering() - test bring-to-front system for multiple media');
console.log(' 10. forceMediaLayering() - FORCE layering to work if broken');
// Debug function for disappearing media
window.debugTouchResize = () => {
console.log('🔍 DEBUGGING TOUCH RESIZE DISAPPEARING ISSUE');
const mediaItems = document.querySelectorAll('.canvas-media-item');
if (mediaItems.length === 0) {
console.log('❌ No media items found in DOM!');
return 'No media items found';
}
mediaItems.forEach((item, index) => {
console.log(`📱 Media Item ${index + 1}:`);
console.log(' - Display:', getComputedStyle(item).display);
console.log(' - Visibility:', getComputedStyle(item).visibility);
console.log(' - Opacity:', getComputedStyle(item).opacity);
console.log(' - Transform:', getComputedStyle(item).transform);
console.log(' - Position:', item.style.position);
console.log(' - Left:', item.style.left);
console.log(' - Top:', item.style.top);
console.log(' - Width:', item.style.width);
console.log(' - Height:', item.style.height);
console.log(' - Z-index:', getComputedStyle(item).zIndex);
console.log(' - Pointer events:', getComputedStyle(item).pointerEvents);
console.log(' - Dataset:', item.dataset);
console.log(' - Parent element:', item.parentElement?.tagName);
console.log(' - Computed bounds:', {
x: getComputedStyle(item).left,
y: getComputedStyle(item).top,
width: getComputedStyle(item).width,
height: getComputedStyle(item).height
});
console.log('---');
});
return 'Check console for detailed media item states';
};
console.log('🔧 Added debugTouchResize() function - call it if media disappears');
// Failsafe function to restore disappeared media
window.restoreDisappearedMedia = () => {
console.log('🚨 RESTORING DISAPPEARED MEDIA');
const mediaItems = document.querySelectorAll('.canvas-media-item');
let restoredCount = 0;
mediaItems.forEach((item, index) => {
const computed = getComputedStyle(item);
// Check if item is hidden or has zero dimensions
if (computed.display === 'none' ||
computed.visibility === 'hidden' ||
computed.opacity === '0' ||
item.offsetWidth === 0 ||
item.offsetHeight === 0) {
console.log(`🔧 Restoring media item ${index + 1}`);
// Force visibility
item.style.display = 'block';
item.style.visibility = 'visible';
item.style.opacity = '1';
// Ensure minimum size if zero
if (item.offsetWidth === 0) {
item.style.width = '300px';
}
if (item.offsetHeight === 0) {
item.style.height = '200px';
}
// Ensure positioned
if (!item.style.position) {
item.style.position = 'absolute';
}
if (!item.style.left) {
item.style.left = '50px';
}
if (!item.style.top) {
item.style.top = '50px';
}
restoredCount++;
}
});
console.log(`✅ Restored ${restoredCount} media items`);
return `Restored ${restoredCount} media items`;
};
console.log('🆘 Added restoreDisappearedMedia() function - emergency restore');
// Test function for pinch vs double-tap conflicts
window.testPinchDoubleTabConflict = () => {
console.log('🔍 TESTING PINCH VS DOUBLE-TAP CONFLICT RESOLUTION');
const mediaItems = document.querySelectorAll('.canvas-media-item');
if (mediaItems.length === 0) {
console.log('❌ No media items found. Add media to test conflicts.');
return 'No media items found';
}
const item = mediaItems[0];
console.log('📱 Simulating pinch gesture...');
// Simulate pinch gesture flags
item._hadMultiTouch = true;
item._isInPinchGesture = true;
item._lastPinchTime = Date.now();
console.log('⚡ Testing double-tap rejection after pinch...');
// Simulate touchend event after pinch
const touchEndEvent = new TouchEvent('touchend', {
touches: [], // No touches remaining
bubbles: true,
cancelable: true
});
// This should be ignored by handleSingleTap
item.dispatchEvent(touchEndEvent);
console.log('✅ Pinch gesture protection tested');
console.log('📱 Test instructions:');
console.log(' 1. Try pinch-to-zoom - should resize without removing media');
console.log(' 2. Try double-tap - should remove media (when not after pinch)');
console.log(' 3. Verify no conflicts between the two gestures');
return 'Pinch vs double-tap conflict test complete - check console';
};
console.log('🧪 Added testPinchDoubleTabConflict() function - test gesture conflicts');
console.log('🆘 Added restoreDisappearedMedia() function - emergency restore');
// Test function for media layering system
window.testMediaLayering = () => {
console.log('🎯 TESTING MEDIA LAYERING SYSTEM');
const mediaItems = document.querySelectorAll('.canvas-media-item');
if (mediaItems.length < 2) {
console.log('❌ Need at least 2 media items to test layering. Add more media.');
return 'Need at least 2 media items for layering test';
}
console.log(`📱 Found ${mediaItems.length} media items for layering test`);
// Show current z-index values
mediaItems.forEach((item, index) => {
const zIndex = getComputedStyle(item).zIndex;
console.log(` Item ${index + 1}: z-index = ${zIndex}`);
});
console.log('✅ Media layering system active:');
console.log(' • Base media layer: z-index 100');
console.log(' • Active media layer: z-index 200 (when interacted with)');
console.log(' • Annotation layer: z-index 300 (always on top)');
console.log('📱 Test instructions:');
console.log(' 1. Click/touch any media item - it should come to the front');
console.log(' 2. Click/touch another media item - it should come to the front');
console.log(' 3. Draw annotations - they should always stay visible on top');
console.log(' 4. Move/resize media - active item stays on top until you interact with another');
return 'Media layering system ready - test by interacting with different media items';
};
// Test media layering system
window.testMediaLayering = () => {
console.log('🎯 Testing media layering system...');
const mediaItems = document.querySelectorAll('.canvas-media-item');
if (mediaItems.length < 2) {
console.log('❌ Need at least 2 media items to test layering. Add more media first.');
return;
}
console.log(`📊 Found ${mediaItems.length} media items. Testing layering:`);
// Show current z-index values
mediaItems.forEach((item, index) => {
const zIndex = window.getComputedStyle(item).zIndex;
const rect = item.getBoundingClientRect();
console.log(`${index + 1}. Media item z-index: ${zIndex} (${Math.round(rect.width)}x${Math.round(rect.height)})`);
});
// Test bringing each item to front
console.log('\n🧪 Testing bring-to-front functionality:');
mediaItems.forEach((item, index) => {
const initialZIndex = window.getComputedStyle(item).zIndex;
// Simulate clicking the item to bring it to front
if (window.localMediaHandler) {
window.localMediaHandler.bringMediaToTop(item);
const newZIndex = window.getComputedStyle(item).zIndex;
console.log(`${index + 1}. Brought to front: ${initialZIndex} → ${newZIndex} ✅`);
}
});
console.log('\n✅ Media layering test complete!');
console.log('💡 Instructions:');
console.log(' 1. Switch to SELECT tool');
console.log(' 2. Click any media item - it comes to front and STAYS there');
console.log(' 3. Move cursor off - layering PRESERVED (no revert to newest)');
console.log(' 4. Interact with different media - only YOUR interactions change layering');
console.log(' 5. First imported, last imported - all have equal priority');
console.log(' 6. Annotations always stay above all media');
};
console.log('🎯 Added testMediaLayering() function - test bring-to-front system');
console.log('🆘 Added restoreDisappearedMedia() function - emergency restore');
// Debug function to visualize clickable areas and fix invisible borders
// Test aspect ratio adjustments
window.testAspectRatioFix = () => {
console.log('🧪 Testing aspect ratio adjustments...');
const mediaItems = document.querySelectorAll('.canvas-media-item');
if (mediaItems.length === 0) {
console.log('❌ No media items found. Add some images first.');
return;
}
console.log(`📊 Found ${mediaItems.length} media items to test:`);
mediaItems.forEach((item, index) => {
const img = item.querySelector('img');
if (!img) {
console.log(`${index + 1}. Not an image - skipping`);
return;
}
const containerRect = item.getBoundingClientRect();
const naturalWidth = img.naturalWidth;
const naturalHeight = img.naturalHeight;
if (naturalWidth === 0 || naturalHeight === 0) {
console.log(`${index + 1}. Image not loaded yet - skipping`);
return;
}
const imageAspect = naturalWidth / naturalHeight;
const containerAspect = containerRect.width / containerRect.height;
const aspectDiff = Math.abs(imageAspect - containerAspect);
console.log(`${index + 1}. Image ${naturalWidth}x${naturalHeight} (aspect: ${imageAspect.toFixed(2)})`);
console.log(` Container ${Math.round(containerRect.width)}x${Math.round(containerRect.height)} (aspect: ${containerAspect.toFixed(2)})`);
if (aspectDiff < 0.01) {
console.log(` ✅ Perfect aspect ratio match - no empty space!`);
} else {
console.log(` ⚠️ Aspect ratio mismatch - will have empty space`);
console.log(` 🔧 Run fixMediaBoundaries() to fix this item`);
// Apply fix immediately if handler is available
if (window.localMediaHandler) {
window.localMediaHandler.adjustContainerAspectRatio(item, img);
console.log(` ✅ Fixed aspect ratio automatically`);
}
}
console.log('---');
});
console.log('✅ Aspect ratio test complete!');
console.log('💡 All images should now have containers that match their aspect ratios exactly');
};
window.debugMediaBoundaries = () => {
console.log('🔍 DEBUGGING MEDIA BOUNDARIES AND INVISIBLE BORDERS');
const mediaItems = document.querySelectorAll('.canvas-media-item');
if (mediaItems.length === 0) {
console.log('❌ No media items found to debug');
return 'No media items found';
}
console.log(`📱 Found ${mediaItems.length} media items to analyze`);
mediaItems.forEach((item, index) => {
const rect = item.getBoundingClientRect();
const img = item.querySelector('img');
const iframe = item.querySelector('iframe');
const content = img || iframe;
console.log(`🎯 Media Item ${index + 1}:`);
console.log(` Container: ${rect.width}x${rect.height}`);
console.log(` Position: (${item.style.left}, ${item.style.top})`);
console.log(` Styles: width=${item.style.width}, height=${item.style.height}`);
if (content) {
const contentRect = content.getBoundingClientRect();
console.log(` Content: ${contentRect.width}x${contentRect.height}`);
console.log(` Content styles: ${content.style.cssText}`);
// Check for invisible borders (container larger than content)
const widthDiff = rect.width - contentRect.width;
const heightDiff = rect.height - contentRect.height;
if (widthDiff > 5 || heightDiff > 5) {
console.log(` ⚠️ INVISIBLE BORDER DETECTED: +${widthDiff}px width, +${heightDiff}px height`);
} else {
console.log(` ✅ Good boundary alignment`);
}
}
// Add visual debug border temporarily
item.style.outline = '2px solid red';
if (content) {
content.style.outline = '2px solid blue';
}
console.log('---');
});
console.log('🔴 Red outline = clickable container area');
console.log('🔵 Blue outline = actual content area');
console.log('❌ If red is much larger than blue, you have invisible borders');
// Remove debug borders after 5 seconds
setTimeout(() => {
mediaItems.forEach(item => {
item.style.outline = '';
const content = item.querySelector('img') || item.querySelector('iframe');
if (content) content.style.outline = '';
});
console.log('🔧 Debug borders removed');
}, 5000);
return 'Debug borders applied for 5 seconds - check for red vs blue alignment';
};
// Function to fix invisible borders on existing media items
window.fixMediaBoundaries = () => {
console.log('🔧 FIXING INVISIBLE MEDIA BOUNDARIES');
const mediaItems = document.querySelectorAll('.canvas-media-item');
if (mediaItems.length === 0) {
console.log('❌ No media items found to fix');
return 'No media items found';
}
let fixedCount = 0;
mediaItems.forEach((item, index) => {
console.log(`🔧 Fixing media item ${index + 1}`);
// Remove any minimum size constraints
item.style.minWidth = '';
item.style.minHeight = '';
item.style.margin = '0';
item.style.padding = '0';
item.style.border = 'none';
// Fix content styling and adjust aspect ratio
const img = item.querySelector('img');
const iframe = item.querySelector('iframe');
if (img) {
// For images, fix styling and adjust aspect ratio
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'contain'; // Show full image without cropping
img.style.margin = '0';
img.style.padding = '0';
img.style.border = 'none';
img.style.display = 'block';
// Adjust container aspect ratio to eliminate empty space
if (img.complete && img.naturalWidth > 0) {
this.adjustContainerAspectRatio(item, img);
console.log(` ✅ Fixed image boundaries and aspect ratio`);
} else {
// Set up onload handler for images that aren't loaded yet
img.onload = () => {
this.adjustContainerAspectRatio(item, img);
console.log(` ✅ Image loaded and aspect ratio fixed`);
};
console.log(` ⏳ Image loading - will fix aspect ratio when ready`);
}
fixedCount++;
} else if (iframe) {
// For iframes, standard styling
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.margin = '0';
iframe.style.padding = '0';
iframe.style.border = 'none';
iframe.style.display = 'block';
fixedCount++;
console.log(` ✅ Fixed iframe styling`);
} else {
console.log(` ⚠️ No content found in media item`);
}
});
console.log(`✅ Fixed ${fixedCount} media items - invisible borders removed`);
console.log('📱 Test by clicking near the edges of media items');
return `Fixed ${fixedCount} media items - boundaries now match content`;
};
// Force media layering to work
window.forceMediaLayering = () => {
console.log('💪 FORCING all media items to have proper layering...');
const mediaItems = document.querySelectorAll('.canvas-media-item');
if (mediaItems.length === 0) {
console.log('❌ No media items found to fix layering');
return;
}
// Debug: Show current z-index values BEFORE any changes
console.log('📊 Current z-index values BEFORE forceMediaLayering:');
mediaItems.forEach((item, index) => {
const currentZ = parseInt(item.style.zIndex) || 100;
console.log(` Item ${index + 1}: z-index ${currentZ}`);
});
console.log(`📊 Found ${mediaItems.length} media items - fixing positioning only, preserving z-index:`);
// Force all items to have proper positioning but PRESERVE z-index
mediaItems.forEach((item, index) => {
// Ensure position is absolute for z-index to work
item.style.position = 'absolute';
item.style.setProperty('position', 'absolute', 'important');
// PRESERVE existing z-index - don't change layering order
const currentZIndex = parseInt(item.style.zIndex) || 100;
// Only set base z-index if it's currently at default or lower
if (currentZIndex <= 100) {
const baseZIndex = 100; // All base items get same z-index
item.style.zIndex = baseZIndex.toString();
item.style.setProperty('z-index', baseZIndex.toString(), 'important');
console.log(` ${index + 1}. Set base z-index: ${baseZIndex}`);
} else {
// Keep existing higher z-index
item.style.setProperty('z-index', currentZIndex.toString(), 'important');
console.log(` ${index + 1}. Preserved existing z-index: ${currentZIndex}`);
}
// Only force position, not z-index
item.style.cssText += `; position: absolute !important;`;
});
console.log('💡 PRESERVED all user layering choices - no automatic reordering');
console.log('✅ FORCED layering complete - all media should now layer properly');
console.log('💡 Try clicking media items now - they should come to front');
};
console.log('🔍 Added debugMediaBoundaries() function - visualize clickable areas');
console.log('🧪 Added testAspectRatioFix() function - test and fix aspect ratio mismatches');
console.log('🔧 Added fixMediaBoundaries() function - fix invisible borders');
console.log('💪 Added forceMediaLayering() function - force layering to work');
// Auto-fix any existing boundary issues and force layering
setTimeout(() => {
const mediaItems = document.querySelectorAll('.canvas-media-item');
if (mediaItems.length > 0) {
console.log('🔧 Auto-fixing any existing media boundary issues...');
window.fixMediaBoundaries();
console.log('💪 Auto-forcing layering for existing media items...');
window.forceMediaLayering();
}
}, 1000);
console.log('=== TEST COMPLETE ===');
}
deleteAnnotationsOnMediaItem(mediaItem) {
// Get the media item's bounds
const mediaRect = mediaItem.getBoundingClientRect();
const mediaCanvas = document.getElementById('mediaCanvas');
const canvasContent = mediaCanvas?.querySelector('.canvas-content');
if (!canvasContent) {
console.error('[ERROR] Canvas content not found for annotation deletion');
return;
}
const canvasRect = canvasContent.getBoundingClientRect();
// Calculate media item bounds relative to canvas
const mediaLeft = mediaRect.left - canvasRect.left;
const mediaTop = mediaRect.top - canvasRect.top;
const mediaRight = mediaLeft + mediaRect.width;
const mediaBottom = mediaTop + mediaRect.height;
console.log('[DEBUG] Deleting annotations on media item:', {
mediaLeft, mediaTop, mediaRight, mediaBottom,
width: mediaRect.width, height: mediaRect.height
});
// Get the current tab ID
const activeTabId = window.bibleApp?.activeTabId;
if (!activeTabId) {
console.error('[ERROR] No active tab ID found');
return;
}
// Get all strokes for this tab
const passageKey = this.getCurrentPassageKey(activeTabId);
const strokes = JSON.parse(localStorage.getItem(`mediaStrokes_${activeTabId}_${passageKey}`) || '[]');
// Filter out strokes that are on this media item
const strokesToKeep = strokes.filter(stroke => {
// Check if any point in the stroke is within the media item bounds
return !stroke.points.some(point => {
const x = point.x / window.devicePixelRatio;
const y = point.y / window.devicePixelRatio;
return x >= mediaLeft && x <= mediaRight && y >= mediaTop && y <= mediaBottom;
});
});
// Save the filtered strokes back to localStorage
localStorage.setItem(`mediaStrokes_${activeTabId}_${passageKey}`, JSON.stringify(strokesToKeep));
// Redraw the canvas to reflect the changes
this.redrawAllStrokes(activeTabId);
console.log('[DEBUG] Deleted annotations on media item. Kept:', strokesToKeep.length, 'of', strokes.length, 'strokes');
}
getCurrentPassageKey(tabId) {
// Get the current passage key for the tab
const tab = window.bibleApp?.tabs?.find(t => t.id === tabId);
if (!tab) return 'default';
// For media tabs, use a consistent key
if (tab.mode === 'media') {
return 'media-canvas';
}
// For bible tabs, use the passage key
const { book, chapter, verse, verseEnd, translation } = tab;
return `${book}_${chapter}_${verse}_${verseEnd || verse}_${translation}`;
}
}
// GLOBAL TEST FUNCTIONS - available in console
window.forceOpenFilePicker = () => {
console.log('[FORCE] Forcing file picker to open...');
const fileInput = document.getElementById('fileInput');
if (fileInput) {
console.log('[FORCE] Found file input, triggering click');
fileInput.click();
} else {
console.log('[FORCE] File input not found, creating new one');
const newInput = document.createElement('input');
newInput.type = 'file';
newInput.multiple = true;
newInput.accept = 'image/*,video/*,audio/*,.pdf';
newInput.style.display = 'none';
newInput.addEventListener('change', (e) => {
console.log('[FORCE] Files selected:', e.target.files);
if (window.localMediaHandler) {
window.localMediaHandler.handleFileSelection(e.target.files);
}
document.body.removeChild(newInput);
});
document.body.appendChild(newInput);
newInput.click();
}
};
// TEST FUNCTION - manually trigger the media button click
window.testMediaButtonClick = () => {
console.log('[TEST] Manually testing media button click...');
const refreshBtn = document.getElementById('refreshMediaBtn');
if (refreshBtn) {
console.log('[TEST] Found media button, triggering click');
console.log('[TEST] Button element:', refreshBtn);
console.log('[TEST] Button onclick:', refreshBtn.onclick);
console.log('[TEST] Button event listeners:', refreshBtn.onclick ? 'Has onclick' : 'No onclick');
refreshBtn.click();
} else {
console.log('[TEST] Media button not found');
}
};
// ULTRA SIMPLE TEST - create a completely new button
window.createTestButton = () => {
console.log('[TEST] Creating a completely new test button...');
const testBtn = document.createElement('button');
testBtn.textContent = 'TEST BUTTON - CLICK ME';
testBtn.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 999999;
background: red;
color: white;
padding: 20px 28px;
font-size: 18px;
border-radius: 10px;
border: 2px solid #fff;
cursor: pointer;
`;
testBtn.onclick = () => {
alert('TEST BUTTON CLICKED!');
window.forceOpenFilePicker();
};
document.body.appendChild(testBtn);
console.log('[TEST] Test button created and added to page');
};
// DEBUG FUNCTION - check all media-related elements
window.debugMediaElements = () => {
console.log('[DEBUG] Checking all media elements...');
const refreshBtn = document.getElementById('refreshMediaBtn');
const fileInput = document.getElementById('fileInput');
const mediaCanvas = document.getElementById('mediaCanvas');
console.log('[DEBUG] refreshMediaBtn:', refreshBtn);
console.log('[DEBUG] fileInput:', fileInput);
console.log('[DEBUG] mediaCanvas:', mediaCanvas);
if (refreshBtn) {
console.log('[DEBUG] Button styles:', {
pointerEvents: refreshBtn.style.pointerEvents,
cursor: refreshBtn.style.cursor,
display: refreshBtn.style.display,
visibility: refreshBtn.style.visibility,
zIndex: refreshBtn.style.zIndex
});
console.log('[DEBUG] Button computed styles:', window.getComputedStyle(refreshBtn));
}
};
// TEST FUNCTION - check eraser tool status
window.debugEraserTool = () => {
console.log('[DEBUG] Checking eraser tool status...');
console.log('[DEBUG] window.currentTool:', window.currentTool);
const eraserBtn = document.querySelector('.tool-icon[data-tool="erase"]');
console.log('[DEBUG] eraserBtn:', eraserBtn);
if (eraserBtn) {
console.log('[DEBUG] eraserBtn.classList:', eraserBtn.classList);
console.log('[DEBUG] eraserBtn.classList.contains("active"):', eraserBtn.classList.contains('active'));
}
// Test the isEraserActive logic
const isEraserActive = window.currentTool === 'erase' ||
(window.currentTool === undefined &&
document.querySelector('.tool-icon[data-tool="erase"]')?.classList.contains('active'));
console.log('[DEBUG] isEraserActive:', isEraserActive);
};
// TEST FUNCTION - manually set eraser tool
window.setEraserTool = () => {
console.log('[DEBUG] Manually setting eraser tool...');
window.currentTool = 'erase';
console.log('[DEBUG] window.currentTool set to:', window.currentTool);
// Also update the UI if possible
const eraserBtn = document.querySelector('.tool-icon[data-tool="erase"]');
if (eraserBtn) {
document.querySelectorAll('.tool-icon').forEach(btn => btn.classList.remove('active'));
eraserBtn.classList.add('active');
console.log('[DEBUG] UI updated to show eraser as active');
}
};
// TEST FUNCTION - check media persistence
window.debugMediaPersistence = () => {
console.log('[DEBUG] Checking media persistence...');
if (!window.bibleApp) {
console.log('[DEBUG] BibleApp not found');
return;
}
window.bibleApp.tabs.forEach(tab => {
console.log(`[DEBUG] Tab ${tab.id} (${tab.mode}):`, {
mediaItems: tab.mediaItems ? tab.mediaItems.length : 0,
mediaItemsData: tab.mediaItems || []
});
});
const activeTab = window.bibleApp.tabs.find(t => t.id === window.bibleApp.activeTabId);
if (activeTab) {
console.log('[DEBUG] Active tab:', activeTab.id, 'Mode:', activeTab.mode);
console.log('[DEBUG] Active tab media items:', activeTab.mediaItems || []);
}
};
// TEST FUNCTION - force restore media items
window.forceRestoreMedia = () => {
console.log('[DEBUG] Force restoring media items...');
if (window.localMediaHandler && window.bibleApp && window.bibleApp.activeTabId) {
window.localMediaHandler.restoreMediaItems(window.bibleApp.activeTabId);
} else {
console.log('[DEBUG] Cannot restore media - missing dependencies');
}
};
// TEST FUNCTION - force re-enable all media items
window.forceReenableMediaItems = () => {
console.log('[DEBUG] Force re-enabling all media items...');
const mediaItems = document.querySelectorAll('.canvas-media-item');
console.log('[DEBUG] Found', mediaItems.length, 'media items to re-enable');
mediaItems.forEach((item, index) => {
console.log(`[DEBUG] Re-enabling media item ${index + 1}:`, item);
// Force remove any existing event listeners
const newItem = item.cloneNode(true);
item.parentNode.replaceChild(newItem, item);
// Force set styles
newItem.style.setProperty('pointer-events', 'auto', 'important');
newItem.style.setProperty('cursor', 'grab', 'important');
newItem.style.setProperty('user-select', 'none', 'important');
newItem.style.setProperty('-webkit-user-select', 'none', 'important');
newItem.style.setProperty('z-index', '10', 'important');
// Re-attach event listeners
if (window.localMediaHandler) {
window.localMediaHandler.makeMediaItemInteractive(newItem);
}
console.log(`[DEBUG] Media item ${index + 1} re-enabled`);
});
// Force update canvas pointer events
if (typeof updateCanvasPointerEvents === 'function') {
updateCanvasPointerEvents();
}
console.log('[DEBUG] All media items re-enabled');
};
// TEST FUNCTION - check media item interactivity
window.checkMediaItemInteractivity = () => {
console.log('[DEBUG] Checking media item interactivity...');
const mediaItems = document.querySelectorAll('.canvas-media-item');
console.log('[DEBUG] Found', mediaItems.length, 'media items');
mediaItems.forEach((item, index) => {
const styles = window.getComputedStyle(item);
console.log(`[DEBUG] Media item ${index + 1}:`, {
pointerEvents: styles.pointerEvents,
cursor: styles.cursor,
zIndex: styles.zIndex,
userSelect: styles.userSelect,
webkitUserSelect: styles.webkitUserSelect
});
});
// Check if there are any annotation canvases that might be interfering
const annotationCanvases = document.querySelectorAll('.annotation-canvas');
console.log('[DEBUG] Found', annotationCanvases.length, 'annotation canvases');
annotationCanvases.forEach((canvas, index) => {
const styles = window.getComputedStyle(canvas);
console.log(`[DEBUG] Annotation canvas ${index + 1}:`, {
pointerEvents: styles.pointerEvents,
zIndex: styles.zIndex
});
});
// Check current tool state
console.log('[DEBUG] Current tool state:', {
currentTool: window.currentTool,
selectModeActive: window.selectModeActive
});
};
// TEST FUNCTION - clean up annotation canvases from media tabs
window.cleanupAnnotationCanvasesFromMediaTabs = () => {
console.log('[DEBUG] Cleaning up annotation canvases from media tabs...');
if (!window.bibleApp) {
console.log('[DEBUG] BibleApp not found');
return;
}
let cleanedCount = 0;
// Check all tabs for media mode
window.bibleApp.tabs.forEach(tab => {
if (tab.mode === 'media') {
const readingArea = document.getElementById(`readingArea${tab.id}`);
if (readingArea) {
const annotationCanvases = readingArea.querySelectorAll('.annotation-canvas, .annotation-overlay-canvas');
annotationCanvases.forEach(canvas => {
canvas.remove();
cleanedCount++;
console.log(`[DEBUG] Removed annotation canvas from media tab ${tab.id}`);
});
}
}
});
console.log(`[DEBUG] Cleaned up ${cleanedCount} annotation canvases from media tabs`);
// Also force re-enable media items after cleanup
if (typeof window.forceReenableMediaItems === 'function') {
setTimeout(() => {
window.forceReenableMediaItems();
}, 100);
}
};
// TEST FUNCTION - check current tab media items
window.checkCurrentTabMedia = () => {
console.log('[DEBUG] Checking current tab media items...');
if (!window.bibleApp || !window.bibleApp.activeTabId) {
console.log('[DEBUG] No active tab');
return;
}
const tab = window.bibleApp.tabs.find(t => t.id === window.bibleApp.activeTabId);
if (!tab) {
console.log('[DEBUG] Active tab not found');
return;
}
console.log('[DEBUG] Current tab:', tab.id, 'Mode:', tab.mode);
console.log('[DEBUG] Media items:', tab.mediaItems || []);
console.log('[DEBUG] Media items count:', tab.mediaItems ? tab.mediaItems.length : 0);
// Also check DOM
const mediaCanvas = document.getElementById('mediaCanvas');
if (mediaCanvas) {
const mediaItems = mediaCanvas.querySelectorAll('.canvas-media-item');
console.log('[DEBUG] DOM media items count:', mediaItems.length);
} else {
console.log('[DEBUG] Media canvas not found in DOM');
}
};