// Customer attachment gestures and image viewing; upload ownership stays on the server. var ChatAttachments = { bindUpload(options){ const zone = options.zone, hint = options.hint; let dragDepth = 0; function hasFiles(event){ return event.dataTransfer && Array.from(event.dataTransfer.types || []).includes('Files'); } function clearDrag(){ dragDepth = 0; hint.hidden = true; } function hasFolder(data){ return Array.from(data.items || []).some(function(item){ const entry = typeof item.webkitGetAsEntry === 'function' ? item.webkitGetAsEntry() : null; return entry && entry.isDirectory; }); } function accept(files){ if(!files.length) return; if(files.length !== 1){ options.status(options.text('singleAttachment'), true); return; } options.upload(files[0]); } options.input.addEventListener('paste', function(event){ const data = event.clipboardData; if(!data) return; if(hasFolder(data)){ event.preventDefault(); options.status(options.text('folderAttachment'), true); return; } let files = Array.from(data.files || []); if(!files.length){ files = Array.from(data.items || []).filter(item=>item.kind === 'file').map(item=>item.getAsFile()).filter(Boolean); } if(!files.length) return; event.preventDefault(); accept(files); }); zone.addEventListener('dragenter', function(event){ if(!hasFiles(event)) return; event.preventDefault(); dragDepth += 1; hint.hidden = false; }); zone.addEventListener('dragover', function(event){ if(!hasFiles(event)) return; event.preventDefault(); event.dataTransfer.dropEffect = 'copy'; }); zone.addEventListener('dragleave', function(event){ if(!dragDepth) return; dragDepth = Math.max(0, dragDepth - 1); if(!dragDepth) clearDrag(); }); zone.addEventListener('drop', function(event){ clearDrag(); if(!hasFiles(event)) return; event.preventDefault(); if(hasFolder(event.dataTransfer)){ options.status(options.text('folderAttachment'), true); return; } accept(Array.from(event.dataTransfer.files || [])); }); // Dropping outside the chat must not navigate away and lose a draft. document.addEventListener('dragover', function(event){ if(hasFiles(event)) event.preventDefault(); }); document.addEventListener('drop', function(event){ clearDrag(); if(hasFiles(event)) event.preventDefault(); }); document.addEventListener('dragend', clearDrag); window.addEventListener('blur', clearDrag); return {clear:clearDrag}; }, createViewer(options){ let active = null; function close(){ if(active) active.close(); } function open(source, trigger){ if(source.hidden || source.dataset.unavailable) return; close(); const dialog = document.createElement('dialog'); dialog.className = 'image-viewer'; dialog.setAttribute('aria-label', options.text('imagePreview')); const header = document.createElement('div'); header.className = 'image-viewer-header'; const title = document.createElement('h2'); title.textContent = options.text('imagePreview'); header.appendChild(title); const controls = document.createElement('div'); controls.className = 'image-viewer-controls'; header.appendChild(controls); const stage = document.createElement('div'); stage.className = 'image-viewer-stage'; const image = document.createElement('img'); image.alt = source.alt || options.text('imagePreview'); const status = document.createElement('p'); status.className = 'image-viewer-status'; status.setAttribute('role', 'status'); status.textContent = options.text('imageLoading'); stage.appendChild(image); stage.appendChild(status); dialog.appendChild(header); dialog.appendChild(stage); let scale = 1, loaded = false, finished = false; function button(key, action){ const control = document.createElement('button'); control.type = 'button'; control.textContent = options.text(key); control.setAttribute('aria-label', options.text(key)); control.onclick = action; controls.appendChild(control); return control; } const smaller = button('imageZoomOut', ()=>zoom(scale - .5)); const larger = button('imageZoomIn', ()=>zoom(scale + .5)); const fit = button('imageFit', ()=>zoom(1)); const dismiss = button('imageClose', ()=>dialog.close()); function zoom(next){ scale = Math.max(1, Math.min(3, next)); smaller.disabled = !loaded || scale <= 1; larger.disabled = !loaded || scale >= 3; fit.disabled = !loaded; if(!loaded) return; const ratio = Math.min(1, Math.max(1, stage.clientWidth - 32) / image.naturalWidth, Math.max(1, stage.clientHeight - 32) / image.naturalHeight); image.style.width = Math.round(image.naturalWidth * ratio * scale) + 'px'; image.style.height = Math.round(image.naturalHeight * ratio * scale) + 'px'; } function resized(){ zoom(scale); } function cleanup(){ if(finished) return; finished = true; window.removeEventListener('resize', resized); dialog.remove(); if(active === dialog){ active = null; if(trigger.isConnected) trigger.focus({preventScroll:true}); } } dialog.addEventListener('close', cleanup); dialog.addEventListener('cancel', function(event){ event.preventDefault(); dialog.close(); }); dialog.addEventListener('click', function(event){ if(event.target === dialog) dialog.close(); }); image.onload = function(){ loaded = true; status.hidden = true; zoom(1); }; image.onerror = function(){ loaded = false; image.hidden = true; status.hidden = false; status.textContent = options.text('attachmentUnavailable'); zoom(1); }; zoom(1); document.body.appendChild(dialog); active = dialog; dialog.showModal(); dismiss.focus({preventScroll:true}); image.src = source.currentSrc || source.src; window.addEventListener('resize', resized); } options.container.addEventListener('click', function(event){ if(event.ctrlKey || event.metaKey || event.shiftKey || event.altKey || event.button > 0) return; const trigger = event.target.closest('a, .image-preview-trigger'); if(!trigger) return; const source = trigger.querySelector('img'); if(!source || (!source.classList.contains('message-image') && !trigger.classList.contains('image-preview-trigger'))) return; event.preventDefault(); open(source, trigger); }); return {close:close}; } };