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