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