169 lines
7.8 KiB
JavaScript
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};
|
|
}
|
|
};
|