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

219 lines
10 KiB
JavaScript

// Shared controls for the customer page and service workspace.
var supportModalQueue = Promise.resolve();
var supportModalId = 0;
var SupportUI = {
confirm(message, options){
return this.openModal(message, options, false);
},
prompt(message, options){
return this.openModal(message, options, true);
},
openModal(message, options, withInput){
options = options || {};
const invoker = document.activeElement;
return new Promise(function(resolve){
supportModalQueue = supportModalQueue.then(function(){
return new Promise(function(finishQueue){
const overlay = document.createElement('div');
overlay.className = 'app-modal-overlay';
const dialog = document.createElement('section');
dialog.className = 'app-modal';
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-modal', 'true');
dialog.setAttribute('tabindex', '-1');
const id = ++supportModalId;
const title = document.createElement('h2');
title.className = 'app-modal-title';
title.id = 'app-modal-title-' + id;
title.textContent = options.title || (withInput ? '请输入内容' : '请确认');
dialog.setAttribute('aria-labelledby', title.id);
dialog.appendChild(title);
if(message){
const detail = document.createElement('div');
detail.className = 'app-modal-message';
detail.id = 'app-modal-message-' + id;
detail.textContent = message;
dialog.setAttribute('aria-describedby', detail.id);
dialog.appendChild(detail);
}
let input = null;
if(withInput){
input = document.createElement(options.multiline ? 'textarea' : 'input');
input.className = 'app-modal-input';
input.setAttribute('aria-label', options.inputLabel || (typeof message === 'string' ? message : title.textContent));
if(!options.multiline) input.type = 'text';
input.value = options.value || '';
if(options.placeholder) input.placeholder = options.placeholder;
dialog.appendChild(input);
}
const actions = document.createElement('div');
actions.className = 'app-modal-actions';
const cancel = document.createElement('button');
cancel.type = 'button';
cancel.textContent = options.cancelText || '取消';
const confirm = document.createElement('button');
confirm.type = 'button';
confirm.className = 'app-modal-confirm';
confirm.textContent = options.confirmText || '确定';
actions.appendChild(cancel);
actions.appendChild(confirm);
dialog.appendChild(actions);
overlay.appendChild(dialog);
let closed = false;
function close(value){
if(closed) return;
closed = true;
overlay.remove();
if(invoker && invoker.isConnected !== false && document.body.contains(invoker)){
invoker.focus({preventScroll:true});
}
resolve(value);
finishQueue();
}
function accept(){ close(withInput ? input.value : true); }
cancel.onclick = function(){ close(withInput ? null : false); };
confirm.onclick = accept;
overlay.onclick = function(event){
if(event.target === overlay) close(withInput ? null : false);
};
dialog.addEventListener('keydown', function(event){
if(event.key === 'Escape'){
event.preventDefault();
close(withInput ? null : false);
return;
}
if(event.key === 'Enter' && (!input || !options.multiline || event.ctrlKey || event.metaKey)){
event.preventDefault();
accept();
return;
}
if(event.key !== 'Tab') return;
const focusable = dialog.querySelectorAll('button, input, textarea');
if(!focusable.length){ event.preventDefault(); dialog.focus(); return; }
const first = focusable[0], last = focusable[focusable.length - 1];
if(event.shiftKey && (document.activeElement === first || !dialog.contains(document.activeElement))){
event.preventDefault(); last.focus();
}else if(!event.shiftKey && (document.activeElement === last || !dialog.contains(document.activeElement))){
event.preventDefault(); first.focus();
}
});
document.body.appendChild(overlay);
(input || cancel).focus({preventScroll:true});
});
});
});
},
initAttachments(container, label){
container.addEventListener('error', function(event){
const image = event.target;
if(!image || image.tagName !== 'IMG' || !image.classList.contains('message-image') || image.dataset.unavailable) return;
image.dataset.unavailable = '1';
image.hidden = true;
const hint = document.createElement('span');
hint.className = 'attachment-unavailable';
hint.textContent = typeof label === 'function' ? label() : label;
image.after(hint);
}, true);
},
async enableNotifications(button){
if(!('Notification' in window)) return;
try{
const permission = await Notification.requestPermission();
button.setAttribute('aria-pressed', String(permission === 'granted'));
}catch(error){}
},
notify(title, body){
if(!document.hidden || !('Notification' in window) || Notification.permission !== 'granted') return;
try{
const notice = new Notification(title, {body:body});
notice.onclick = function(){ window.focus(); notice.close(); };
}catch(error){}
},
addRating(container, sid, labels){
const request = labels.fetch || fetch;
const box = document.createElement('section');
box.className = 'service-rating';
const question = document.createElement('p');
question.textContent = labels.question;
box.appendChild(question);
const choices = document.createElement('div');
choices.className = 'rating-choices';
box.appendChild(choices);
const status = document.createElement('p');
status.setAttribute('role', 'status');
box.appendChild(status);
let sending = false;
[1, 2, 3, 4, 5].forEach(function(stars){
const button = document.createElement('button');
button.type = 'button';
button.textContent = '★';
button.setAttribute('aria-label', stars+' / 5');
button.onmouseenter = function(){
Array.from(choices.children).forEach(function(item, index){ item.classList.toggle('selected', index < stars); });
};
button.onclick = async function(){
if(sending) return;
sending = true;
Array.from(choices.children).forEach(function(item){ item.disabled = true; });
try{
const response = await request('/api/rate', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({sid:sid, stars:stars, comment:''})});
const data = await response.json();
if(!response.ok || !data.ok) throw new Error('Rating failed');
box.textContent = labels.thanks+' '+ '★'.repeat(stars);
}catch(error){
sending = false;
status.textContent = labels.failed;
Array.from(choices.children).forEach(function(item){ item.disabled = false; });
}
};
choices.appendChild(button);
});
container.appendChild(box);
return box;
},
initEmoji(button, panel, input, onInsert){
const emojis = ['😊', '😂', '👍', '❤️', '🙏', '😅', '😢', '🎉', '📦', '🚚'];
function close(){
panel.hidden = true;
button.setAttribute('aria-expanded', 'false');
}
button.onclick = function(){
panel.hidden = !panel.hidden;
button.setAttribute('aria-expanded', String(!panel.hidden));
};
emojis.forEach(function(emoji){
const choice = document.createElement('button');
choice.type = 'button';
choice.textContent = emoji;
choice.onclick = function(){
const start = input.selectionStart == null ? input.value.length : input.selectionStart;
const end = input.selectionEnd == null ? start : input.selectionEnd;
input.value = input.value.slice(0, start) + emoji + input.value.slice(end);
close();
input.focus({preventScroll:true});
input.setSelectionRange(start + emoji.length, start + emoji.length);
if(onInsert) onInsert();
};
panel.appendChild(choice);
});
['pointerdown','mousedown'].forEach(function(name){
document.addEventListener(name, function(event){
if(!panel.hidden && !panel.contains(event.target) && !button.contains(event.target)) close();
});
});
document.addEventListener('keydown', function(event){
if(event.key === 'Escape' && !panel.hidden){
close();
button.focus({preventScroll:true});
}
});
return {close:close};
}
};