// 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 = `
+
`; // 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'); } };