Files
baiyue-logistics-chat/static/js/attachments.js
T

169 lines
7.8 KiB
JavaScript

// 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};
}
};