From 9e5318fdb5dc32a66828835f3799e3aa199d0bb8 Mon Sep 17 00:00:00 2001 From: Felix Niklas Date: Tue, 22 Jan 2019 07:12:32 +0100 Subject: [PATCH] Private fn chat no jquery --- public/assets/chat/chat-no-jquery.coffee | 1468 ++++++++++ public/assets/chat/chat-no-jquery.js | 2490 +++++++++++++++++ public/assets/chat/chat-no-jquery.min.js | 2 + public/assets/chat/chat.css | 46 +- public/assets/chat/chat.scss | 6 + public/assets/chat/gulpfile.js | 22 + .../chat/znuny-no-jquery-open_by_button.html | 174 ++ public/assets/chat/znuny-no-jquery.html | 173 ++ public/assets/chat/znuny.html | 1 - script/build/test_slice_tests.sh | 6 + test/browser/chat_no_jquery_test.rb | 864 ++++++ 11 files changed, 5210 insertions(+), 42 deletions(-) create mode 100644 public/assets/chat/chat-no-jquery.coffee create mode 100644 public/assets/chat/chat-no-jquery.js create mode 100644 public/assets/chat/chat-no-jquery.min.js create mode 100644 public/assets/chat/znuny-no-jquery-open_by_button.html create mode 100644 public/assets/chat/znuny-no-jquery.html create mode 100644 test/browser/chat_no_jquery_test.rb diff --git a/public/assets/chat/chat-no-jquery.coffee b/public/assets/chat/chat-no-jquery.coffee new file mode 100644 index 000000000..a959cf17d --- /dev/null +++ b/public/assets/chat/chat-no-jquery.coffee @@ -0,0 +1,1468 @@ +do(window) -> + + scripts = document.getElementsByTagName('script') + + # search for script to get protocol and hostname for ws connection + myScript = scripts[scripts.length - 1] + scriptProtocol = window.location.protocol.replace(':', '') # set default protocol + if myScript && myScript.src + scriptHost = myScript.src.match('.*://([^:/]*).*')[1] + scriptProtocol = myScript.src.match('(.*)://[^:/]*.*')[1] + + # Define the plugin class + class Core + defaults: + debug: false + + constructor: (options) -> + @options = {} + + for key, value of @defaults + @options[key] = value + + for key, value of options + @options[key] = value + + class Base extends Core + constructor: (options) -> + super(options) + + @log = new Log(debug: @options.debug, logPrefix: @options.logPrefix || @logPrefix) + + class Log extends Core + debug: (items...) => + return if !@options.debug + @log('debug', items) + + notice: (items...) => + @log('notice', items) + + error: (items...) => + @log('error', items) + + log: (level, items) => + items.unshift('||') + items.unshift(level) + items.unshift(@options.logPrefix) + console.log.apply console, items + + return if !@options.debug + logString = '' + for item in items + logString += ' ' + if typeof item is 'object' + logString += JSON.stringify(item) + else if item && item.toString + logString += item.toString() + else + logString += item + element = document.querySelector('.js-chatLogDisplay') + if element + element.innerHTML = '
' + logString + '
' + element.innerHTML + + class Timeout extends Base + timeoutStartedAt: null + logPrefix: 'timeout' + defaults: + debug: false + timeout: 4 + timeoutIntervallCheck: 0.5 + + start: => + @stop() + timeoutStartedAt = new Date + check = => + timeLeft = new Date - new Date(timeoutStartedAt.getTime() + @options.timeout * 1000 * 60) + @log.debug "Timeout check for #{@options.timeout} minutes (left #{timeLeft/1000} sec.)"#, new Date + return if timeLeft < 0 + @stop() + @options.callback() + @log.debug "Start timeout in #{@options.timeout} minutes"#, new Date + @intervallId = setInterval(check, @options.timeoutIntervallCheck * 1000 * 60) + + stop: => + return if !@intervallId + @log.debug "Stop timeout of #{@options.timeout} minutes"#, new Date + clearInterval(@intervallId) + + class Io extends Base + logPrefix: 'io' + + set: (params) => + for key, value of params + @options[key] = value + + connect: => + @log.debug "Connecting to #{@options.host}" + @ws = new window.WebSocket("#{@options.host}") + @ws.onopen = (e) => + @log.debug 'onOpen', e + @options.onOpen(e) + @ping() + + @ws.onmessage = (e) => + pipes = JSON.parse(e.data) + @log.debug 'onMessage', e.data + for pipe in pipes + if pipe.event is 'pong' + @ping() + if @options.onMessage + @options.onMessage(pipes) + + @ws.onclose = (e) => + @log.debug 'close websocket connection', e + if @pingDelayId + clearTimeout(@pingDelayId) + if @manualClose + @log.debug 'manual close, onClose callback' + @manualClose = false + if @options.onClose + @options.onClose(e) + else + @log.debug 'error close, onError callback' + if @options.onError + @options.onError('Connection lost...') + + @ws.onerror = (e) => + @log.debug 'onError', e + if @options.onError + @options.onError(e) + + close: => + @log.debug 'close websocket manually' + @manualClose = true + @ws.close() + + reconnect: => + @log.debug 'reconnect' + @close() + @connect() + + send: (event, data = {}) => + @log.debug 'send', event, data + msg = JSON.stringify + event: event + data: data + @ws.send msg + + ping: => + localPing = => + @send('ping') + @pingDelayId = setTimeout(localPing, 29000) + + class ZammadChat extends Base + defaults: + chatId: undefined + show: true + target: document.querySelector('body') + host: '' + debug: false + flat: false + lang: undefined + cssAutoload: true + cssUrl: undefined + fontSize: undefined + buttonClass: 'open-zammad-chat' + inactiveClass: 'is-inactive' + title: 'Chat with us!' + scrollHint: 'Scroll down to see new messages' + idleTimeout: 6 + idleTimeoutIntervallCheck: 0.5 + inactiveTimeout: 8 + inactiveTimeoutIntervallCheck: 0.5 + waitingListTimeout: 4 + waitingListTimeoutIntervallCheck: 0.5 + + logPrefix: 'chat' + _messageCount: 0 + isOpen: false + blinkOnlineInterval: null + stopBlinOnlineStateTimeout: null + showTimeEveryXMinutes: 2 + lastTimestamp: null + lastAddedType: null + inputTimeout: null + isTyping: false + state: 'offline' + initialQueueDelay: 10000 + translations: + 'de': + 'Chat with us!': 'Chatte mit uns!' + 'Scroll down to see new messages': 'Scrolle nach unten um neue Nachrichten zu sehen' + 'Online': 'Online' + 'Offline': 'Offline' + 'Connecting': 'Verbinden' + 'Connection re-established': 'Verbindung wiederhergestellt' + 'Today': 'Heute' + 'Send': 'Senden' + 'Chat closed by %s': 'Chat beendet von %s' + 'Compose your message...': 'Ihre Nachricht...' + 'All colleagues are busy.': 'Alle Kollegen sind belegt.' + 'You are on waiting list position %s.': 'Sie sind in der Warteliste an der Position %s.' + 'Start new conversation': 'Neue Konversation starten' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!' + 'es': + 'Chat with us!': 'Chatee con nosotros!' + 'Scroll down to see new messages': 'Haga scroll hacia abajo para ver nuevos mensajes' + 'Online': 'En linea' + 'Offline': 'Desconectado' + 'Connecting': 'Conectando' + 'Connection re-established': 'Conexión restablecida' + 'Today': 'Hoy' + 'Send': 'Enviar' + 'Chat closed by %s': 'Chat cerrado por %s' + 'Compose your message...': 'Escriba su mensaje...' + 'All colleagues are busy.': 'Todos los agentes están ocupados.' + 'You are on waiting list position %s.': 'Usted está en la posición %s de la lista de espera.' + 'Start new conversation': 'Iniciar nueva conversación' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!' + 'fr': + 'Chat with us!': 'Chattez avec nous!' + 'Scroll down to see new messages': 'Faites défiler pour lire les nouveaux messages' + 'Online': 'En-ligne' + 'Offline': 'Hors-ligne' + 'Connecting': 'Connexion en cours' + 'Connection re-established': 'Connexion rétablie' + 'Today': 'Aujourdhui' + 'Send': 'Envoyer' + 'Chat closed by %s': 'Chat fermé par %s' + 'Compose your message...': 'Composez votre message...' + 'All colleagues are busy.': 'Tous les collègues sont actuellement occupés.' + 'You are on waiting list position %s.': 'Vous êtes actuellement en %s position dans la file d\'attente.' + 'Start new conversation': 'Démarrer une nouvelle conversation' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Si vous ne répondez pas dans les %s minutes, votre conversation avec %s va être fermée.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Je vous remercie!' + 'nl': + 'Chat with us!': 'Chat met ons!' + 'Scroll down to see new messages': 'Scrol naar beneden om nieuwe berichten te zien' + 'Online': 'Online' + 'Offline': 'Offline' + 'Connecting': 'Verbinden' + 'Connection re-established': 'Verbinding herstelt' + 'Today': 'Vandaag' + 'Send': 'Verzenden' + 'Chat closed by %s': 'Chat gesloten door %s' + 'Compose your message...': 'Typ uw bericht...' + 'All colleagues are busy.': 'Alle medewerkers zijn bezet.' + 'You are on waiting list position %s.': 'U bent %s in de wachtrij.' + 'Start new conversation': 'Nieuwe conversatie starten' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!' + 'it': + 'Chat with us!': 'Chatta con noi!' + 'Scroll down to see new messages': 'Scorrere verso il basso per vedere i nuovi messaggi' + 'Online': 'Online' + 'Offline': 'Offline' + 'Connecting': 'Collegamento' + 'Connection re-established': 'Collegamento ristabilito' + 'Today': 'Oggi' + 'Send': 'Invio' + 'Chat closed by %s': 'Conversazione chiusa da %s' + 'Compose your message...': 'Comporre il tuo messaggio...' + 'All colleagues are busy.': 'Tutti i colleghi sono occupati.' + 'You are on waiting list position %s.': 'Siete in posizione lista d\' attesa %s.' + 'Start new conversation': 'Avviare una nuova conversazione' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione con %s si è chiusa.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione si è chiusa.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Ci dispiace, ci vuole più tempo come previsto per ottenere uno slot vuoto. Per favore riprova più tardi o inviaci un\' e-mail. Grazie!' + 'pl': + 'Chat with us!': 'Czatuj z nami!' + 'Scroll down to see new messages': 'Przewiń w dół, aby wyświetlić nowe wiadomości' + 'Online': 'Online' + 'Offline': 'Offline' + 'Connecting': 'Łączenie' + 'Connection re-established': 'Ponowne nawiązanie połączenia' + 'Today': 'dzisiejszy' + 'Send': 'Wyślij' + 'Chat closed by %s': 'Czat zamknięty przez %s' + 'Compose your message...': 'Utwórz swoją wiadomość...' + 'All colleagues are busy.': 'Wszyscy koledzy są zajęci.' + 'You are on waiting list position %s.': 'Na liście oczekujących znajduje się pozycja %s.' + 'Start new conversation': 'Rozpoczęcie nowej konwersacji' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z %s została zamknięta.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!' + 'zh-cn': + 'Chat with us!': '发起即时对话!' + 'Scroll down to see new messages': '向下滚动以查看新消息' + 'Online': '在线' + 'Offline': '离线' + 'Connecting': '连接中' + 'Connection re-established': '正在重新建立连接' + 'Today': '今天' + 'Send': '发送' + 'Chat closed by %s': 'Chat closed by %s' + 'Compose your message...': '正在输入信息...' + 'All colleagues are busy.': '所有工作人员都在忙碌中.' + 'You are on waiting list position %s.': '您目前的等候位置是第 %s 位.' + 'Start new conversation': '开始新的会话' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': '由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': '由于您超过 %s 分钟没有任何回复, 该对话已被关闭.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': '非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!' + 'zh-tw': + 'Chat with us!': '開始即時對话!' + 'Scroll down to see new messages': '向下滑動以查看新訊息' + 'Online': '線上' + 'Offline': '离线' + 'Connecting': '連線中' + 'Connection re-established': '正在重新建立連線中' + 'Today': '今天' + 'Send': '發送' + 'Chat closed by %s': 'Chat closed by %s' + 'Compose your message...': '正在輸入訊息...' + 'All colleagues are busy.': '所有服務人員都在忙碌中.' + 'You are on waiting list position %s.': '你目前的等候位置是第 %s 順位.' + 'Start new conversation': '開始新的對話' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': '由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': '由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': '非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!' + 'ru': + 'Chat with us!': 'Напишите нам!' + 'Scroll down to see new messages': 'Прокрутите, чтобы увидеть новые сообщения' + 'Online': 'Онлайн' + 'Offline': 'Оффлайн' + 'Connecting': 'Подключение' + 'Connection re-established': 'Подключение восстановлено' + 'Today': 'Сегодня' + 'Send': 'Отправить' + 'Chat closed by %s': '%s закрыл чат' + 'Compose your message...': 'Напишите сообщение...' + 'All colleagues are busy.': 'Все сотрудники заняты' + 'You are on waiting list position %s.': 'Вы в списке ожидания под номером %s' + 'Start new conversation': 'Начать новую переписку.' + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.' + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.' + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!' + sessionId: undefined + scrolledToBottom: true + scrollSnapTolerance: 10 + richTextFormatKey: + 66: true # b + 73: true # i + 85: true # u + 83: true # s + + T: (string, items...) => + if @options.lang && @options.lang isnt 'en' + if !@translations[@options.lang] + @log.notice "Translation '#{@options.lang}' needed!" + else + translations = @translations[@options.lang] + if !translations[string] + @log.notice "Translation needed for '#{string}'" + string = translations[string] || string + if items + for item in items + string = string.replace(/%s/, item) + string + + view: (name) => + return (options) => + if !options + options = {} + + options.T = @T + options.background = @options.background + options.flat = @options.flat + options.fontSize = @options.fontSize + return window.zammadChatTemplates[name](options) + + constructor: (options) -> + super(options) + + # jQuery migration + if typeof jQuery != 'undefined' && @options.target instanceof jQuery + @log.notice 'Chat: target option is a jQuery object. jQuery is not a requirement for the chat any more.' + @options.target = @options.target.get(0) + + # fullscreen + @isFullscreen = (window.matchMedia and window.matchMedia('(max-width: 768px)').matches) + @scrollRoot = @getScrollRoot() + + # check prerequisites + if !window.WebSocket or !sessionStorage + @state = 'unsupported' + @log.notice 'Chat: Browser not supported!' + return + if !@options.chatId + @state = 'unsupported' + @log.error 'Chat: need chatId as option!' + return + + # detect language + if !@options.lang + @options.lang = document.documentElement.getAttribute('lang') + if @options.lang + if !@translations[@options.lang] + @log.debug "lang: No #{@options.lang} found, try first two letters" + @options.lang = @options.lang.replace(/-.+?$/, '') # replace "-xx" of xx-xx + @log.debug "lang: #{@options.lang}" + + # detect host + @detectHost() if !@options.host + + @loadCss() + + @io = new Io(@options) + @io.set( + onOpen: @render + onClose: @onWebSocketClose + onMessage: @onWebSocketMessage + onError: @onError + ) + + @io.connect() + + getScrollRoot: -> + return document.scrollingElement if 'scrollingElement' of document + html = document.documentElement + start = parseInt(html.pageYOffset, 10) + html.pageYOffset = start + 1 + end = parseInt(html.pageYOffset, 10) + html.pageYOffset = start + return if end > start then html else document.body + + render: => + if !@el || !document.querySelector('.zammad-chat') + @renderBase() + + # disable open button + document.querySelector(".#{ @options.buttonClass }").classList.add @inactiveClass + + @setAgentOnlineState 'online' + + @log.debug 'widget rendered' + + @startTimeoutObservers() + @idleTimeout.start() + + # get current chat status + @sessionId = sessionStorage.getItem('sessionId') + @send 'chat_status_customer', + session_id: @sessionId + url: window.location.href + + renderBase: -> + @el.remove() if @el + @options.target.innerHTML += @view('chat')( + title: @options.title, + scrollHint: @options.scrollHint + ) + @el = @options.target.querySelector('.zammad-chat') + @input = @el.querySelector('.zammad-chat-input') + @body = @el.querySelector('.zammad-chat-body') + + # start bindings + @el.querySelector('.js-chat-open').addEventListener('click', @open) + @el.querySelector('.js-chat-toggle').addEventListener('click', @toggle) + @el.querySelector('.js-chat-status').addEventListener('click', @stopPropagation) + @el.querySelector('.zammad-chat-controls').addEventListener('submit', @onSubmit) + @body.addEventListener('scroll', @detectScrolledtoBottom) + @el.querySelector('.zammad-scroll-hint').addEventListener('click', @onScrollHintClick) + @input.addEventListener('keydown', @onKeydown) + @input.addEventListener('input', @onInput) + @input.addEventListener('paste', @onPaste) + @input.addEventListener('drop', @onDrop) + + window.addEventListener('beforeunload', @onLeaveTemporary) + window.addEventListener('hashchange', => + if @isOpen + if @sessionId + @send 'chat_session_notice', + session_id: @sessionId + message: window.location.href + return + @idleTimeout.start() + ) + + stopPropagation: (event) -> + event.stopPropagation() + + onPaste: (e) => + e.stopPropagation() + e.preventDefault() + + if window.dataTransfer # ie + dataTransfer = window.dataTransfer + else if e.dataTransfer # other browsers + dataTransfer = e.dataTransfer + else + throw 'No clipboardData support' + + x = e.clientX + y = e.clientY + file = dataTransfer.files[0] + + # look for images + if file.type.match('image.*') + reader = new FileReader() + reader.onload = (e) => + # Insert the image at the carat + insert = (dataUrl, width) => + + # adapt image if we are on retina devices + if @isRetina() + width = width / 2 + + result = dataUrl + img = new Image() + img.style.width = '100%' + img.style.maxWidth = width +'px' + img.src = result + + if document.caretPositionFromPoint + pos = document.caretPositionFromPoint(x, y) + range = document.createRange() + range.setStart(pos.offsetNode, pos.offset) + range.collapse() + range.insertNode(img) + else if document.caretRangeFromPoint + range = document.caretRangeFromPoint(x, y) + range.insertNode(img) + else + console.log('could not find carat') + + # resize if to big + @resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert) + reader.readAsDataURL(file) + + onPaste: (e) => + e.stopPropagation() + e.preventDefault() + + if e.clipboardData + clipboardData = e.clipboardData + else if window.clipboardData + clipboardData = window.clipboardData + else if e.clipboardData + clipboardData = e.clipboardData + else + throw 'No clipboardData support' + + imageInserted = false + if clipboardData && clipboardData.items && clipboardData.items[0] + item = clipboardData.items[0] + if item.kind == 'file' && (item.type == 'image/png' || item.type == 'image/jpeg') + imageFile = item.getAsFile() + reader = new FileReader() + + reader.onload = (e) => + insert = (dataUrl, width) => + + # adapt image if we are on retina devices + if @isRetina() + width = width / 2 + + img = new Image() + img.style.width = '100%' + img.style.maxWidth = width +'px' + img.src = dataUrl + document.execCommand('insertHTML', false, img) + + # resize if to big + @resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert) + + reader.readAsDataURL(imageFile) + imageInserted = true + + return if imageInserted + + # check existing + paste text for limit + text = undefined + docType = undefined + try + text = clipboardData.getData('text/html') + docType = 'html' + if !text || text.length is 0 + docType = 'text' + text = clipboardData.getData('text/plain') + if !text || text.length is 0 + docType = 'text2' + text = clipboardData.getData('text') + catch e + console.log('Sorry, can\'t insert markup because browser is not supporting it.') + docType = 'text3' + text = clipboardData.getData('text') + + if docType is 'text' || docType is 'text2' || docType is 'text3' + text = '
' + text.replace(/\n/g, '
') + '
' + text = text.replace(/
<\/div>/g, '

') + console.log('p', docType, text) + if docType is 'html' + html = document.createElement('div') + html.innerHTML = text + match = false + htmlTmp = text + regex = new RegExp('<(/w|w)\:[A-Za-z]') + if htmlTmp.match(regex) + match = true + htmlTmp = htmlTmp.replace(regex, '') + regex = new RegExp('<(/o|o)\:[A-Za-z]') + if htmlTmp.match(regex) + match = true + htmlTmp = htmlTmp.replace(regex, '') + if match + html = @wordFilter(html) + #html + + for node in html.childNodes + if node.nodeType == 8 + node.remove() + + # remove tags, keep content + for node in html.querySelectorAll('a, font, small, time, form, label') + node.outerHTML = node.innerHTML + + # replace tags with generic div + # New type of the tag + replacementTag = 'div'; + + # Replace all x tags with the type of replacementTag + for node in html.querySelectorAll('textarea') + outer = node.outerHTML + + # Replace opening tag + regex = new RegExp('<' + node.tagName, 'i') + newTag = outer.replace(regex, '<' + replacementTag) + + # Replace closing tag + regex = new RegExp(' + # check for enter + if not e.shiftKey and e.keyCode is 13 + e.preventDefault() + @sendMessage() + + richtTextControl = false + if !e.altKey && !e.ctrlKey && e.metaKey + richtTextControl = true + else if !e.altKey && e.ctrlKey && !e.metaKey + richtTextControl = true + + if richtTextControl && @richTextFormatKey[ e.keyCode ] + e.preventDefault() + if e.keyCode is 66 + document.execCommand('bold') + return true + if e.keyCode is 73 + document.execCommand('italic') + return true + if e.keyCode is 85 + document.execCommand('underline') + return true + if e.keyCode is 83 + document.execCommand('strikeThrough') + return true + + send: (event, data = {}) => + data.chat_id = @options.chatId + @io.send(event, data) + + onWebSocketMessage: (pipes) => + for pipe in pipes + @log.debug 'ws:onmessage', pipe + switch pipe.event + when 'chat_error' + @log.notice pipe.data + if pipe.data && pipe.data.state is 'chat_disabled' + @destroy(remove: true) + when 'chat_session_message' + return if pipe.data.self_written + @receiveMessage pipe.data + when 'chat_session_typing' + return if pipe.data.self_written + @onAgentTypingStart() + when 'chat_session_start' + @onConnectionEstablished pipe.data + when 'chat_session_queue' + @onQueueScreen pipe.data + when 'chat_session_closed' + @onSessionClosed pipe.data + when 'chat_session_left' + @onSessionClosed pipe.data + when 'chat_status_customer' + switch pipe.data.state + when 'online' + @sessionId = undefined + + if !@options.cssAutoload || @cssLoaded + @onReady() + else + @socketReady = true + when 'offline' + @onError 'Zammad Chat: No agent online' + when 'chat_disabled' + @onError 'Zammad Chat: Chat is disabled' + when 'no_seats_available' + @onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}" + when 'reconnect' + @onReopenSession pipe.data + + onReady: -> + @log.debug 'widget ready for use' + btn = document.querySelector(".#{ @options.buttonClass }") + if btn + btn.addEventListener('click', @open) + btn.classList.remove(@inactiveClass) + + if @options.show + @show() + + onError: (message) => + @log.debug message + @addStatus(message) + document.querySelector(".#{ @options.buttonClass }").classList.add('zammad-chat-is-hidden') + if @isOpen + @disableInput() + @destroy(remove: false) + else + @destroy(remove: true) + + onReopenSession: (data) => + @log.debug 'old messages', data.session + @inactiveTimeout.start() + + unfinishedMessage = sessionStorage.getItem 'unfinished_message' + + # rerender chat history + if data.agent + @onConnectionEstablished(data) + + for message in data.session + @renderMessage + message: message.content + id: message.id + from: if message.created_by_id then 'agent' else 'customer' + + if unfinishedMessage + @input.innerHTML = unfinishedMessage + + # show wait list + if data.position + @onQueue data + + @show() + @open() + @scrollToBottom() + + if unfinishedMessage + @input.focus() + + onInput: => + # remove unread-state from messages + for message in @el.querySelectorAll('.zammad-chat-message--unread') + node.classList.remove 'zammad-chat-message--unread' + + sessionStorage.setItem 'unfinished_message', @input.innerHTML + + @onTyping() + + onTyping: -> + + # send typing start event only every 1.5 seconds + return if @isTyping && @isTyping > new Date(new Date().getTime() - 1500) + @isTyping = new Date() + @send 'chat_session_typing', + session_id: @sessionId + @inactiveTimeout.start() + + onSubmit: (event) => + event.preventDefault() + @sendMessage() + + sendMessage: -> + message = @input.innerHTML + return if !message + + @inactiveTimeout.start() + + sessionStorage.removeItem 'unfinished_message' + + messageElement = @view('message') + message: message + from: 'customer' + id: @_messageCount++ + unreadClass: '' + + @maybeAddTimestamp() + + # add message before message typing loader + if @el.querySelector('.zammad-chat-message--typing') + @lastAddedType = 'typing-placeholder' + @el.querySelector('.zammad-chat-message--typing').insertAdjacentHTML('beforebegin', messageElement) + else + @lastAddedType = 'message--customer' + @body.insertAdjacentHTML('beforeend', messageElement) + + @input.innerHTML = '' + @scrollToBottom() + + # send message event + @send 'chat_session_message', + content: message + id: @_messageCount + session_id: @sessionId + + receiveMessage: (data) => + @inactiveTimeout.start() + + # hide writing indicator + @onAgentTypingEnd() + + @maybeAddTimestamp() + + @renderMessage + message: data.message.content + id: data.id + from: 'agent' + + @scrollToBottom showHint: true + + renderMessage: (data) => + @lastAddedType = "message--#{ data.from }" + data.unreadClass = if document.hidden then ' zammad-chat-message--unread' else '' + @body.insertAdjacentHTML('beforeend', @view('message')(data)) + + open: => + if @isOpen + @log.debug 'widget already open, block' + return + + @isOpen = true + @log.debug 'open widget' + @show() + + if !@sessionId + @showLoader() + + @el.classList.add 'zammad-chat-is-open' + remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight + @el.style.transform = "translateY(#{remainerHeight}px)" + # force redraw + @el.clientHeight + + if !@sessionId + @el.addEventListener 'transitionend', @onOpenTransitionend + @el.classList.add 'zammad-chat--animate' + # force redraw + @el.clientHeight + # start animation + @el.style.transform = '' + + @send('chat_session_init' + url: window.location.href + ) + else + @el.style.transform = '' + @onOpenTransitionend() + + onOpenTransitionend: => + @el.removeEventListener 'transitionend', @onOpenTransitionend + @el.classList.remove 'zammad-chat--animate' + @idleTimeout.stop() + + if @isFullscreen + @disableScrollOnRoot() + + sessionClose: => + # send close + @send 'chat_session_close', + session_id: @sessionId + + # stop timer + @inactiveTimeout.stop() + @waitingListTimeout.stop() + + # delete input store + sessionStorage.removeItem 'unfinished_message' + + # stop delay of initial queue position + if @onInitialQueueDelayId + clearTimeout(@onInitialQueueDelayId) + + @setSessionId undefined + + toggle: (event) => + if @isOpen + @close(event) + else + @open(event) + + close: (event) => + if !@isOpen + @log.debug 'can\'t close widget, it\'s not open' + return + if @initDelayId + clearTimeout(@initDelayId) + if !@sessionId + @log.debug 'can\'t close widget without sessionId' + return + + @log.debug 'close widget' + + event.stopPropagation() if event + + @sessionClose() + + if @isFullscreen + @enableScrollOnRoot() + + # close window + remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight + @el.addEventListener 'transitionend', @onCloseTransitionend + @el.classList.add 'zammad-chat--animate' + # force redraw + document.offsetHeight + # animate out + @el.style.transform = "translateY(#{remainerHeight}px)" + + onCloseTransitionend: => + @el.removeEventListener 'transitionend', @onCloseTransitionend + @el.classList.remove 'zammad-chat-is-open', 'zammad-chat--animate' + @el.style.transform = '' + + @showLoader() + @el.querySelector('.zammad-chat-welcome').classList.remove('zammad-chat-is-hidden') + @el.querySelector('.zammad-chat-agent').classList.add('zammad-chat-is-hidden') + @el.querySelector('.zammad-chat-agent-status').classList.add('zammad-chat-is-hidden') + + @isOpen = false + + @io.reconnect() + + onWebSocketClose: => + return if @isOpen + if @el + @el.classList.remove('zammad-chat-is-shown') + @el.classList.remove('zammad-chat-is-loaded') + + show: -> + return if @state is 'offline' + + @el.classList.add('zammad-chat-is-loaded') + @el.classList.add('zammad-chat-is-shown') + + disableInput: -> + @input.disabled = true + @el.querySelector('.zammad-chat-send').disabled = true + + enableInput: -> + @input.disabled = false + @el.querySelector('.zammad-chat-send').disabled = false + + hideModal: -> + @el.querySelector('.zammad-chat-modal').innerHTML = '' + + onQueueScreen: (data) => + @setSessionId data.session_id + + # delay initial queue position, show connecting first + show = => + @onQueue data + @waitingListTimeout.start() + + if @initialQueueDelay && !@onInitialQueueDelayId + @onInitialQueueDelayId = setTimeout(show, @initialQueueDelay) + return + + # stop delay of initial queue position + if @onInitialQueueDelayId + clearTimeout(@onInitialQueueDelayId) + + # show queue position + show() + + onQueue: (data) => + @log.notice 'onQueue', data.position + @inQueue = true + + @el.querySelector('.zammad-chat-modal').innerHTML = @view('waiting') + position: data.position + + onAgentTypingStart: => + if @stopTypingId + clearTimeout(@stopTypingId) + @stopTypingId = setTimeout(@onAgentTypingEnd, 3000) + + # never display two typing indicators + return if @el.querySelector('.zammad-chat-message--typing') + + @maybeAddTimestamp() + + @body.insertAdjacentHTML('beforeend', @view('typingIndicator')()) + + # only if typing indicator is shown + return if !@isVisible(@el.querySelector('.zammad-chat-message--typing'), true) + @scrollToBottom() + + onAgentTypingEnd: => + @el.querySelector('.zammad-chat-message--typing').remove() if @el.querySelector('.zammad-chat-message--typing') + + onLeaveTemporary: => + return if !@sessionId + @send 'chat_session_leave_temporary', + session_id: @sessionId + + maybeAddTimestamp: -> + timestamp = Date.now() + + if !@lastTimestamp or (timestamp - @lastTimestamp) > @showTimeEveryXMinutes * 60000 + label = @T('Today') + time = new Date().toTimeString().substr 0,5 + if @lastAddedType is 'timestamp' + # update last time + @updateLastTimestamp label, time + @lastTimestamp = timestamp + else + # add new timestamp + @body.insertAdjacentHTML 'beforeend', @view('timestamp') + label: label + time: time + @lastTimestamp = timestamp + @lastAddedType = 'timestamp' + @scrollToBottom() + + updateLastTimestamp: (label, time) -> + return if !@el + timestamps = @el.querySelectorAll('.zammad-chat-body .zammad-chat-timestamp') + return if !timestamps + timestamps[timestamps.length - 1].outerHTML = @view('timestamp') + label: label + time: time + + addStatus: (status) -> + return if !@el + @maybeAddTimestamp() + + @body.insertAdjacentHTML 'beforeend', @view('status') + status: status + + @scrollToBottom() + + detectScrolledtoBottom: => + scrollBottom = @body.scrollTop + @body.offsetHeight + @scrolledToBottom = Math.abs(scrollBottom - @body.scrollHeight) <= @scrollSnapTolerance + @el.querySelector('.zammad-scroll-hint').classList.add('is-hidden') if @scrolledToBottom + + showScrollHint: -> + @el.querySelector('.zammad-scroll-hint').classList.remove('is-hidden') + # compensate scroll + @body.scrollTop = @body.scrollTop + @el.querySelector('.zammad-scroll-hint').offsetHeight + + onScrollHintClick: => + # animate scroll + @body.scrollTo + top: @body.scrollHeight + behavior: 'smooth' + + scrollToBottom: ({ showHint } = { showHint: false }) -> + if @scrolledToBottom + @body.scrollTop = @body.scrollHeight + else if showHint + @showScrollHint() + + destroy: (params = {}) => + @log.debug 'destroy widget', params + + @setAgentOnlineState 'offline' + + if params.remove && @el + @el.remove() + + # stop all timer + if @waitingListTimeout + @waitingListTimeout.stop() + if @inactiveTimeout + @inactiveTimeout.stop() + if @idleTimeout + @idleTimeout.stop() + + # stop ws connection + @io.close() + + reconnect: => + # set status to connecting + @log.notice 'reconnecting' + @disableInput() + @lastAddedType = 'status' + @setAgentOnlineState 'connecting' + @addStatus @T('Connection lost') + + onConnectionReestablished: => + # set status back to online + @lastAddedType = 'status' + @setAgentOnlineState 'online' + @addStatus @T('Connection re-established') + + onSessionClosed: (data) -> + @addStatus @T('Chat closed by %s', data.realname) + @disableInput() + @setAgentOnlineState 'offline' + @inactiveTimeout.stop() + + setSessionId: (id) => + @sessionId = id + if id is undefined + sessionStorage.removeItem 'sessionId' + else + sessionStorage.setItem 'sessionId', id + + onConnectionEstablished: (data) => + # stop delay of initial queue position + if @onInitialQueueDelayId + clearTimeout @onInitialQueueDelayId + + @inQueue = false + if data.agent + @agent = data.agent + if data.session_id + @setSessionId data.session_id + + # empty old messages + @body.innerHTML = '' + + @el.querySelector('.zammad-chat-agent').innerHTML = @view('agent') + agent: @agent + + @enableInput() + + @hideModal() + @el.querySelector('.zammad-chat-welcome').classList.add('zammad-chat-is-hidden') + @el.querySelector('.zammad-chat-agent').classList.remove('zammad-chat-is-hidden') + @el.querySelector('.zammad-chat-agent-status').classList.remove('zammad-chat-is-hidden') + + @input.focus() if not @isFullscreen + + @setAgentOnlineState 'online' + + @waitingListTimeout.stop() + @idleTimeout.stop() + @inactiveTimeout.start() + + showCustomerTimeout: -> + @el.querySelector('.zammad-chat-modal').innerHTML = @view('customer_timeout') + agent: @agent.name + delay: @options.inactiveTimeout + @el.querySelector('.js-restart').addEventListener 'click', -> location.reload() + @sessionClose() + + showWaitingListTimeout: -> + @el.querySelector('.zammad-chat-modal').innerHTML = @view('waiting_list_timeout') + delay: @options.watingListTimeout + @el.querySelector('.js-restart').addEventListener 'click', -> location.reload() + @sessionClose() + + showLoader: -> + @el.querySelector('.zammad-chat-modal').innerHTML = @view('loader')() + + setAgentOnlineState: (state) => + @state = state + return if !@el + capitalizedState = state.charAt(0).toUpperCase() + state.slice(1) + @el.querySelector('.zammad-chat-agent-status').dataset.status = state + @el.querySelector('.zammad-chat-agent-status').textContent = @T(capitalizedState) + + detectHost: -> + protocol = 'ws://' + if scriptProtocol is 'https' + protocol = 'wss://' + @options.host = "#{ protocol }#{ scriptHost }/ws" + + loadCss: -> + return if !@options.cssAutoload + url = @options.cssUrl + if !url + url = @options.host + .replace(/^wss/i, 'https') + .replace(/^ws/i, 'http') + .replace(/\/ws/i, '') + url += '/assets/chat/chat.css' + + @log.debug "load css from '#{url}'" + styles = "@import url('#{url}');" + newSS = document.createElement('link') + newSS.onload = @onCssLoaded + newSS.rel = 'stylesheet' + newSS.href = 'data:text/css,' + escape(styles) + document.getElementsByTagName('head')[0].appendChild(newSS) + + onCssLoaded: => + @cssLoaded = true + if @socketReady + @onReady() + + startTimeoutObservers: => + @idleTimeout = new Timeout( + logPrefix: 'idleTimeout' + debug: @options.debug + timeout: @options.idleTimeout + timeoutIntervallCheck: @options.idleTimeoutIntervallCheck + callback: => + @log.debug 'Idle timeout reached, hide widget', new Date + @destroy(remove: true) + ) + @inactiveTimeout = new Timeout( + logPrefix: 'inactiveTimeout' + debug: @options.debug + timeout: @options.inactiveTimeout + timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck + callback: => + @log.debug 'Inactive timeout reached, show timeout screen.', new Date + @showCustomerTimeout() + @destroy(remove: false) + ) + @waitingListTimeout = new Timeout( + logPrefix: 'waitingListTimeout' + debug: @options.debug + timeout: @options.waitingListTimeout + timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck + callback: => + @log.debug 'Waiting list timeout reached, show timeout screen.', new Date + @showWaitingListTimeout() + @destroy(remove: false) + ) + + disableScrollOnRoot: -> + @rootScrollOffset = @scrollRoot.scrollTop + @scrollRoot.style.overflow = 'hidden' + @scrollRoot.style.position = 'fixed' + + enableScrollOnRoot: -> + @scrollRoot.scrollTop = @rootScrollOffset +'px' + @scrollRoot.style.overflow = '' + @scrollRoot.style.position = '' + + # based on https://github.com/customd/jquery-visible/blob/master/jquery.visible.js + # to have not dependency, port to coffeescript + isVisible: (el, partial, hidden, direction) -> + return if el.length < 1 + + vpWidth = window.innerWidth + vpHeight = window.innerHeight + direction = if direction then direction else 'both' + clientSize = if hidden is true then t.offsetWidth * t.offsetHeight else true + + rec = el.getBoundingClientRect() + tViz = rec.top >= 0 && rec.top < vpHeight + bViz = rec.bottom > 0 && rec.bottom <= vpHeight + lViz = rec.left >= 0 && rec.left < vpWidth + rViz = rec.right > 0 && rec.right <= vpWidth + vVisible = if partial then tViz || bViz else tViz && bViz + hVisible = if partial then lViz || rViz else lViz && rViz + + if direction is 'both' + return clientSize && vVisible && hVisible + else if direction is 'vertical' + return clientSize && vVisible + else if direction is 'horizontal' + return clientSize && hVisible + + isRetina: -> + if window.matchMedia + mq = window.matchMedia('only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)') + return (mq && mq.matches || (window.devicePixelRatio > 1)) + false + + resizeImage: (dataURL, x = 'auto', y = 'auto', sizeFactor = 1, type, quallity, callback, force = true) -> + + # load image from data url + imageObject = new Image() + imageObject.onload = -> + imageWidth = imageObject.width + imageHeight = imageObject.height + console.log('ImageService', 'current size', imageWidth, imageHeight) + if y is 'auto' && x is 'auto' + x = imageWidth + y = imageHeight + + # get auto dimensions + if y is 'auto' + factor = imageWidth / x + y = imageHeight / factor + + if x is 'auto' + factor = imageWidth / y + x = imageHeight / factor + + # check if resize is needed + resize = false + if x < imageWidth || y < imageHeight + resize = true + x = x * sizeFactor + y = y * sizeFactor + else + x = imageWidth + y = imageHeight + + # create canvas and set dimensions + canvas = document.createElement('canvas') + canvas.width = x + canvas.height = y + + # draw image on canvas and set image dimensions + context = canvas.getContext('2d') + context.drawImage(imageObject, 0, 0, x, y) + + # set quallity based on image size + if quallity == 'auto' + if x < 200 && y < 200 + quallity = 1 + else if x < 400 && y < 400 + quallity = 0.9 + else if x < 600 && y < 600 + quallity = 0.8 + else if x < 900 && y < 900 + quallity = 0.7 + else + quallity = 0.6 + + # execute callback with resized image + newDataUrl = canvas.toDataURL(type, quallity) + if resize + console.log('ImageService', 'resize', x/sizeFactor, y/sizeFactor, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb') + callback(newDataUrl, x/sizeFactor, y/sizeFactor, true) + return + console.log('ImageService', 'no resize', x, y, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb') + callback(newDataUrl, x, y, false) + + # load image from data url + imageObject.src = dataURL + + # taken from https://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294 + pasteHtmlAtCaret: (html) -> + sel = undefined + range = undefined + if window.getSelection + sel = window.getSelection() + if sel.getRangeAt && sel.rangeCount + range = sel.getRangeAt(0) + range.deleteContents() + + el = document.createElement('div') + el.innerHTML = html + frag = document.createDocumentFragment(node, lastNode) + while node = el.firstChild + lastNode = frag.appendChild(node) + range.insertNode(frag) + + if lastNode + range = range.cloneRange() + range.setStartAfter(lastNode) + range.collapse(true) + sel.removeAllRanges() + sel.addRange(range) + else if document.selection && document.selection.type != 'Control' + document.selection.createRange().pasteHTML(html) + + # (C) sbrin - https://github.com/sbrin + # https://gist.github.com/sbrin/6801034 + wordFilter: (editor) -> + content = editor.html() + + # Word comments like conditional comments etc + content = content.replace(//gi, '') + + # Remove comments, scripts (e.g., msoShowComment), XML tag, VML content, + # MS Office namespaced tags, and a few other tags + content = content.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, '') + + # Convert into for line-though + content = content.replace(/<(\/?)s>/gi, '<$1strike>') + + # Replace nbsp entites to char since it's easier to handle + # content = content.replace(/ /gi, "\u00a0") + content = content.replace(/ /gi, ' ') + + # Convert ___ to string of alternating + # breaking/non-breaking spaces of same length + #content = content.replace(/([\s\u00a0]*)<\/span>/gi, (str, spaces) -> + # return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : '' + #) + + editor.innerHTML = content + + # Parse out list indent level for lists + for p in editor.querySelectorAll('p') + str = p.getAttribute('style') + matches = /mso-list:\w+ \w+([0-9]+)/.exec(str) + if matches + p.dataset._listLevel = parseInt(matches[1], 10) + + # Parse Lists + last_level = 0 + pnt = null + for p in editor.querySelectorAll('p') + cur_level = p.dataset._listLevel + if cur_level != undefined + txt = p.textContent + list_tag = '
    ' + if (/^\s*\w+\./.test(txt)) + matches = /([0-9])\./.exec(txt) + if matches + start = parseInt(matches[1], 10) + list_tag = start>1 ? '
      ' : '
        ' + else + list_tag = '
          ' + + if cur_level > last_level + if last_level == 0 + p.insertAdjacentHTML 'beforebegin', list_tag + pnt = p.previousElementSibling + else + pnt.insertAdjacentHTML 'beforeend', list_tag + + if cur_level < last_level + for i in [i..last_level-cur_level] + pnt = pnt.parentNode + + p.querySelector('span:first').remove() if p.querySelector('span:first') + pnt.insertAdjacentHTML 'beforeend', '
        1. ' + p.innerHTML + '
        2. ' + p.remove() + last_level = cur_level + else + last_level = 0 + + el.removeAttribute('style') for el in editor.querySelectorAll('[style]') + el.removeAttribute('align') for el in editor.querySelectorAll('[align]') + el.outerHTML = el.innerHTML for el in editor.querySelectorAll('span') + el.remove() for el in editor.querySelectorAll('span:empty') + el.removeAttribute('class') for el in editor.querySelectorAll("[class^='Mso']") + el.remove() for el in editor.querySelectorAll('p:empty') + editor + + removeAttribute: (element) -> + return if !element + for att in element.attributes + element.removeAttribute(att.name) + + removeAttributes: (html) => + for node in html.querySelectorAll('*') + @removeAttribute node + html + + window.ZammadChat = ZammadChat diff --git a/public/assets/chat/chat-no-jquery.js b/public/assets/chat/chat-no-jquery.js new file mode 100644 index 000000000..d4da85e38 --- /dev/null +++ b/public/assets/chat/chat-no-jquery.js @@ -0,0 +1,2490 @@ +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["agent"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + if (this.agent.avatar) { + __out.push('\n\n'); + } + + __out.push('\n\n '); + + __out.push(__sanitize(this.agent.name)); + + __out.push('\n'); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + +var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + slice = [].slice; + +(function(window) { + var Base, Core, Io, Log, Timeout, ZammadChat, myScript, scriptHost, scriptProtocol, scripts; + scripts = document.getElementsByTagName('script'); + myScript = scripts[scripts.length - 1]; + scriptProtocol = window.location.protocol.replace(':', ''); + if (myScript && myScript.src) { + scriptHost = myScript.src.match('.*://([^:/]*).*')[1]; + scriptProtocol = myScript.src.match('(.*)://[^:/]*.*')[1]; + } + Core = (function() { + Core.prototype.defaults = { + debug: false + }; + + function Core(options) { + var key, ref, value; + this.options = {}; + ref = this.defaults; + for (key in ref) { + value = ref[key]; + this.options[key] = value; + } + for (key in options) { + value = options[key]; + this.options[key] = value; + } + } + + return Core; + + })(); + Base = (function(superClass) { + extend(Base, superClass); + + function Base(options) { + Base.__super__.constructor.call(this, options); + this.log = new Log({ + debug: this.options.debug, + logPrefix: this.options.logPrefix || this.logPrefix + }); + } + + return Base; + + })(Core); + Log = (function(superClass) { + extend(Log, superClass); + + function Log() { + this.log = bind(this.log, this); + this.error = bind(this.error, this); + this.notice = bind(this.notice, this); + this.debug = bind(this.debug, this); + return Log.__super__.constructor.apply(this, arguments); + } + + Log.prototype.debug = function() { + var items; + items = 1 <= arguments.length ? slice.call(arguments, 0) : []; + if (!this.options.debug) { + return; + } + return this.log('debug', items); + }; + + Log.prototype.notice = function() { + var items; + items = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return this.log('notice', items); + }; + + Log.prototype.error = function() { + var items; + items = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return this.log('error', items); + }; + + Log.prototype.log = function(level, items) { + var element, item, j, len, logString; + items.unshift('||'); + items.unshift(level); + items.unshift(this.options.logPrefix); + console.log.apply(console, items); + if (!this.options.debug) { + return; + } + logString = ''; + for (j = 0, len = items.length; j < len; j++) { + item = items[j]; + logString += ' '; + if (typeof item === 'object') { + logString += JSON.stringify(item); + } else if (item && item.toString) { + logString += item.toString(); + } else { + logString += item; + } + } + element = document.querySelector('.js-chatLogDisplay'); + if (element) { + return element.innerHTML = '
          ' + logString + '
          ' + element.innerHTML; + } + }; + + return Log; + + })(Core); + Timeout = (function(superClass) { + extend(Timeout, superClass); + + function Timeout() { + this.stop = bind(this.stop, this); + this.start = bind(this.start, this); + return Timeout.__super__.constructor.apply(this, arguments); + } + + Timeout.prototype.timeoutStartedAt = null; + + Timeout.prototype.logPrefix = 'timeout'; + + Timeout.prototype.defaults = { + debug: false, + timeout: 4, + timeoutIntervallCheck: 0.5 + }; + + Timeout.prototype.start = function() { + var check, timeoutStartedAt; + this.stop(); + timeoutStartedAt = new Date; + check = (function(_this) { + return function() { + var timeLeft; + timeLeft = new Date - new Date(timeoutStartedAt.getTime() + _this.options.timeout * 1000 * 60); + _this.log.debug("Timeout check for " + _this.options.timeout + " minutes (left " + (timeLeft / 1000) + " sec.)"); + if (timeLeft < 0) { + return; + } + _this.stop(); + return _this.options.callback(); + }; + })(this); + this.log.debug("Start timeout in " + this.options.timeout + " minutes"); + return this.intervallId = setInterval(check, this.options.timeoutIntervallCheck * 1000 * 60); + }; + + Timeout.prototype.stop = function() { + if (!this.intervallId) { + return; + } + this.log.debug("Stop timeout of " + this.options.timeout + " minutes"); + return clearInterval(this.intervallId); + }; + + return Timeout; + + })(Base); + Io = (function(superClass) { + extend(Io, superClass); + + function Io() { + this.ping = bind(this.ping, this); + this.send = bind(this.send, this); + this.reconnect = bind(this.reconnect, this); + this.close = bind(this.close, this); + this.connect = bind(this.connect, this); + this.set = bind(this.set, this); + return Io.__super__.constructor.apply(this, arguments); + } + + Io.prototype.logPrefix = 'io'; + + Io.prototype.set = function(params) { + var key, results, value; + results = []; + for (key in params) { + value = params[key]; + results.push(this.options[key] = value); + } + return results; + }; + + Io.prototype.connect = function() { + this.log.debug("Connecting to " + this.options.host); + this.ws = new window.WebSocket("" + this.options.host); + this.ws.onopen = (function(_this) { + return function(e) { + _this.log.debug('onOpen', e); + _this.options.onOpen(e); + return _this.ping(); + }; + })(this); + this.ws.onmessage = (function(_this) { + return function(e) { + var j, len, pipe, pipes; + pipes = JSON.parse(e.data); + _this.log.debug('onMessage', e.data); + for (j = 0, len = pipes.length; j < len; j++) { + pipe = pipes[j]; + if (pipe.event === 'pong') { + _this.ping(); + } + } + if (_this.options.onMessage) { + return _this.options.onMessage(pipes); + } + }; + })(this); + this.ws.onclose = (function(_this) { + return function(e) { + _this.log.debug('close websocket connection', e); + if (_this.pingDelayId) { + clearTimeout(_this.pingDelayId); + } + if (_this.manualClose) { + _this.log.debug('manual close, onClose callback'); + _this.manualClose = false; + if (_this.options.onClose) { + return _this.options.onClose(e); + } + } else { + _this.log.debug('error close, onError callback'); + if (_this.options.onError) { + return _this.options.onError('Connection lost...'); + } + } + }; + })(this); + return this.ws.onerror = (function(_this) { + return function(e) { + _this.log.debug('onError', e); + if (_this.options.onError) { + return _this.options.onError(e); + } + }; + })(this); + }; + + Io.prototype.close = function() { + this.log.debug('close websocket manually'); + this.manualClose = true; + return this.ws.close(); + }; + + Io.prototype.reconnect = function() { + this.log.debug('reconnect'); + this.close(); + return this.connect(); + }; + + Io.prototype.send = function(event, data) { + var msg; + if (data == null) { + data = {}; + } + this.log.debug('send', event, data); + msg = JSON.stringify({ + event: event, + data: data + }); + return this.ws.send(msg); + }; + + Io.prototype.ping = function() { + var localPing; + localPing = (function(_this) { + return function() { + return _this.send('ping'); + }; + })(this); + return this.pingDelayId = setTimeout(localPing, 29000); + }; + + return Io; + + })(Base); + ZammadChat = (function(superClass) { + extend(ZammadChat, superClass); + + ZammadChat.prototype.defaults = { + chatId: void 0, + show: true, + target: document.querySelector('body'), + host: '', + debug: false, + flat: false, + lang: void 0, + cssAutoload: true, + cssUrl: void 0, + fontSize: void 0, + buttonClass: 'open-zammad-chat', + inactiveClass: 'is-inactive', + title: 'Chat with us!', + scrollHint: 'Scroll down to see new messages', + idleTimeout: 6, + idleTimeoutIntervallCheck: 0.5, + inactiveTimeout: 8, + inactiveTimeoutIntervallCheck: 0.5, + waitingListTimeout: 4, + waitingListTimeoutIntervallCheck: 0.5 + }; + + ZammadChat.prototype.logPrefix = 'chat'; + + ZammadChat.prototype._messageCount = 0; + + ZammadChat.prototype.isOpen = false; + + ZammadChat.prototype.blinkOnlineInterval = null; + + ZammadChat.prototype.stopBlinOnlineStateTimeout = null; + + ZammadChat.prototype.showTimeEveryXMinutes = 2; + + ZammadChat.prototype.lastTimestamp = null; + + ZammadChat.prototype.lastAddedType = null; + + ZammadChat.prototype.inputTimeout = null; + + ZammadChat.prototype.isTyping = false; + + ZammadChat.prototype.state = 'offline'; + + ZammadChat.prototype.initialQueueDelay = 10000; + + ZammadChat.prototype.translations = { + 'de': { + 'Chat with us!': 'Chatte mit uns!', + 'Scroll down to see new messages': 'Scrolle nach unten um neue Nachrichten zu sehen', + 'Online': 'Online', + 'Offline': 'Offline', + 'Connecting': 'Verbinden', + 'Connection re-established': 'Verbindung wiederhergestellt', + 'Today': 'Heute', + 'Send': 'Senden', + 'Chat closed by %s': 'Chat beendet von %s', + 'Compose your message...': 'Ihre Nachricht...', + 'All colleagues are busy.': 'Alle Kollegen sind belegt.', + 'You are on waiting list position %s.': 'Sie sind in der Warteliste an der Position %s.', + 'Start new conversation': 'Neue Konversation starten', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!' + }, + 'es': { + 'Chat with us!': 'Chatee con nosotros!', + 'Scroll down to see new messages': 'Haga scroll hacia abajo para ver nuevos mensajes', + 'Online': 'En linea', + 'Offline': 'Desconectado', + 'Connecting': 'Conectando', + 'Connection re-established': 'Conexión restablecida', + 'Today': 'Hoy', + 'Send': 'Enviar', + 'Chat closed by %s': 'Chat cerrado por %s', + 'Compose your message...': 'Escriba su mensaje...', + 'All colleagues are busy.': 'Todos los agentes están ocupados.', + 'You are on waiting list position %s.': 'Usted está en la posición %s de la lista de espera.', + 'Start new conversation': 'Iniciar nueva conversación', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!' + }, + 'fr': { + 'Chat with us!': 'Chattez avec nous!', + 'Scroll down to see new messages': 'Faites défiler pour lire les nouveaux messages', + 'Online': 'En-ligne', + 'Offline': 'Hors-ligne', + 'Connecting': 'Connexion en cours', + 'Connection re-established': 'Connexion rétablie', + 'Today': 'Aujourdhui', + 'Send': 'Envoyer', + 'Chat closed by %s': 'Chat fermé par %s', + 'Compose your message...': 'Composez votre message...', + 'All colleagues are busy.': 'Tous les collègues sont actuellement occupés.', + 'You are on waiting list position %s.': 'Vous êtes actuellement en %s position dans la file d\'attente.', + 'Start new conversation': 'Démarrer une nouvelle conversation', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Si vous ne répondez pas dans les %s minutes, votre conversation avec %s va être fermée.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Je vous remercie!' + }, + 'nl': { + 'Chat with us!': 'Chat met ons!', + 'Scroll down to see new messages': 'Scrol naar beneden om nieuwe berichten te zien', + 'Online': 'Online', + 'Offline': 'Offline', + 'Connecting': 'Verbinden', + 'Connection re-established': 'Verbinding herstelt', + 'Today': 'Vandaag', + 'Send': 'Verzenden', + 'Chat closed by %s': 'Chat gesloten door %s', + 'Compose your message...': 'Typ uw bericht...', + 'All colleagues are busy.': 'Alle medewerkers zijn bezet.', + 'You are on waiting list position %s.': 'U bent %s in de wachtrij.', + 'Start new conversation': 'Nieuwe conversatie starten', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!' + }, + 'it': { + 'Chat with us!': 'Chatta con noi!', + 'Scroll down to see new messages': 'Scorrere verso il basso per vedere i nuovi messaggi', + 'Online': 'Online', + 'Offline': 'Offline', + 'Connecting': 'Collegamento', + 'Connection re-established': 'Collegamento ristabilito', + 'Today': 'Oggi', + 'Send': 'Invio', + 'Chat closed by %s': 'Conversazione chiusa da %s', + 'Compose your message...': 'Comporre il tuo messaggio...', + 'All colleagues are busy.': 'Tutti i colleghi sono occupati.', + 'You are on waiting list position %s.': 'Siete in posizione lista d\' attesa %s.', + 'Start new conversation': 'Avviare una nuova conversazione', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione con %s si è chiusa.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione si è chiusa.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Ci dispiace, ci vuole più tempo come previsto per ottenere uno slot vuoto. Per favore riprova più tardi o inviaci un\' e-mail. Grazie!' + }, + 'pl': { + 'Chat with us!': 'Czatuj z nami!', + 'Scroll down to see new messages': 'Przewiń w dół, aby wyświetlić nowe wiadomości', + 'Online': 'Online', + 'Offline': 'Offline', + 'Connecting': 'Łączenie', + 'Connection re-established': 'Ponowne nawiązanie połączenia', + 'Today': 'dzisiejszy', + 'Send': 'Wyślij', + 'Chat closed by %s': 'Czat zamknięty przez %s', + 'Compose your message...': 'Utwórz swoją wiadomość...', + 'All colleagues are busy.': 'Wszyscy koledzy są zajęci.', + 'You are on waiting list position %s.': 'Na liście oczekujących znajduje się pozycja %s.', + 'Start new conversation': 'Rozpoczęcie nowej konwersacji', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z %s została zamknięta.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!' + }, + 'zh-cn': { + 'Chat with us!': '发起即时对话!', + 'Scroll down to see new messages': '向下滚动以查看新消息', + 'Online': '在线', + 'Offline': '离线', + 'Connecting': '连接中', + 'Connection re-established': '正在重新建立连接', + 'Today': '今天', + 'Send': '发送', + 'Chat closed by %s': 'Chat closed by %s', + 'Compose your message...': '正在输入信息...', + 'All colleagues are busy.': '所有工作人员都在忙碌中.', + 'You are on waiting list position %s.': '您目前的等候位置是第 %s 位.', + 'Start new conversation': '开始新的会话', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': '由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': '由于您超过 %s 分钟没有任何回复, 该对话已被关闭.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': '非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!' + }, + 'zh-tw': { + 'Chat with us!': '開始即時對话!', + 'Scroll down to see new messages': '向下滑動以查看新訊息', + 'Online': '線上', + 'Offline': '离线', + 'Connecting': '連線中', + 'Connection re-established': '正在重新建立連線中', + 'Today': '今天', + 'Send': '發送', + 'Chat closed by %s': 'Chat closed by %s', + 'Compose your message...': '正在輸入訊息...', + 'All colleagues are busy.': '所有服務人員都在忙碌中.', + 'You are on waiting list position %s.': '你目前的等候位置是第 %s 順位.', + 'Start new conversation': '開始新的對話', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': '由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': '由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': '非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!' + }, + 'ru': { + 'Chat with us!': 'Напишите нам!', + 'Scroll down to see new messages': 'Прокрутите, чтобы увидеть новые сообщения', + 'Online': 'Онлайн', + 'Offline': 'Оффлайн', + 'Connecting': 'Подключение', + 'Connection re-established': 'Подключение восстановлено', + 'Today': 'Сегодня', + 'Send': 'Отправить', + 'Chat closed by %s': '%s закрыл чат', + 'Compose your message...': 'Напишите сообщение...', + 'All colleagues are busy.': 'Все сотрудники заняты', + 'You are on waiting list position %s.': 'Вы в списке ожидания под номером %s', + 'Start new conversation': 'Начать новую переписку.', + 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.', + 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.', + 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!' + } + }; + + ZammadChat.prototype.sessionId = void 0; + + ZammadChat.prototype.scrolledToBottom = true; + + ZammadChat.prototype.scrollSnapTolerance = 10; + + ZammadChat.prototype.richTextFormatKey = { + 66: true, + 73: true, + 85: true, + 83: true + }; + + ZammadChat.prototype.T = function() { + var item, items, j, len, string, translations; + string = arguments[0], items = 2 <= arguments.length ? slice.call(arguments, 1) : []; + if (this.options.lang && this.options.lang !== 'en') { + if (!this.translations[this.options.lang]) { + this.log.notice("Translation '" + this.options.lang + "' needed!"); + } else { + translations = this.translations[this.options.lang]; + if (!translations[string]) { + this.log.notice("Translation needed for '" + string + "'"); + } + string = translations[string] || string; + } + } + if (items) { + for (j = 0, len = items.length; j < len; j++) { + item = items[j]; + string = string.replace(/%s/, item); + } + } + return string; + }; + + ZammadChat.prototype.view = function(name) { + return (function(_this) { + return function(options) { + if (!options) { + options = {}; + } + options.T = _this.T; + options.background = _this.options.background; + options.flat = _this.options.flat; + options.fontSize = _this.options.fontSize; + return window.zammadChatTemplates[name](options); + }; + })(this); + }; + + function ZammadChat(options) { + this.removeAttributes = bind(this.removeAttributes, this); + this.startTimeoutObservers = bind(this.startTimeoutObservers, this); + this.onCssLoaded = bind(this.onCssLoaded, this); + this.setAgentOnlineState = bind(this.setAgentOnlineState, this); + this.onConnectionEstablished = bind(this.onConnectionEstablished, this); + this.setSessionId = bind(this.setSessionId, this); + this.onConnectionReestablished = bind(this.onConnectionReestablished, this); + this.reconnect = bind(this.reconnect, this); + this.destroy = bind(this.destroy, this); + this.onScrollHintClick = bind(this.onScrollHintClick, this); + this.detectScrolledtoBottom = bind(this.detectScrolledtoBottom, this); + this.onLeaveTemporary = bind(this.onLeaveTemporary, this); + this.onAgentTypingEnd = bind(this.onAgentTypingEnd, this); + this.onAgentTypingStart = bind(this.onAgentTypingStart, this); + this.onQueue = bind(this.onQueue, this); + this.onQueueScreen = bind(this.onQueueScreen, this); + this.onWebSocketClose = bind(this.onWebSocketClose, this); + this.onCloseTransitionend = bind(this.onCloseTransitionend, this); + this.close = bind(this.close, this); + this.toggle = bind(this.toggle, this); + this.sessionClose = bind(this.sessionClose, this); + this.onOpenTransitionend = bind(this.onOpenTransitionend, this); + this.open = bind(this.open, this); + this.renderMessage = bind(this.renderMessage, this); + this.receiveMessage = bind(this.receiveMessage, this); + this.onSubmit = bind(this.onSubmit, this); + this.onInput = bind(this.onInput, this); + this.onReopenSession = bind(this.onReopenSession, this); + this.onError = bind(this.onError, this); + this.onWebSocketMessage = bind(this.onWebSocketMessage, this); + this.send = bind(this.send, this); + this.onKeydown = bind(this.onKeydown, this); + this.onPaste = bind(this.onPaste, this); + this.onPaste = bind(this.onPaste, this); + this.render = bind(this.render, this); + this.view = bind(this.view, this); + this.T = bind(this.T, this); + ZammadChat.__super__.constructor.call(this, options); + if (typeof jQuery !== 'undefined' && this.options.target instanceof jQuery) { + this.log.notice('Chat: target option is a jQuery object. jQuery is not a requirement for the chat any more.'); + this.options.target = this.options.target.get(0); + } + this.isFullscreen = window.matchMedia && window.matchMedia('(max-width: 768px)').matches; + this.scrollRoot = this.getScrollRoot(); + if (!window.WebSocket || !sessionStorage) { + this.state = 'unsupported'; + this.log.notice('Chat: Browser not supported!'); + return; + } + if (!this.options.chatId) { + this.state = 'unsupported'; + this.log.error('Chat: need chatId as option!'); + return; + } + if (!this.options.lang) { + this.options.lang = document.documentElement.getAttribute('lang'); + } + if (this.options.lang) { + if (!this.translations[this.options.lang]) { + this.log.debug("lang: No " + this.options.lang + " found, try first two letters"); + this.options.lang = this.options.lang.replace(/-.+?$/, ''); + } + this.log.debug("lang: " + this.options.lang); + } + if (!this.options.host) { + this.detectHost(); + } + this.loadCss(); + this.io = new Io(this.options); + this.io.set({ + onOpen: this.render, + onClose: this.onWebSocketClose, + onMessage: this.onWebSocketMessage, + onError: this.onError + }); + this.io.connect(); + } + + ZammadChat.prototype.getScrollRoot = function() { + var end, html, start; + if ('scrollingElement' in document) { + return document.scrollingElement; + } + html = document.documentElement; + start = parseInt(html.pageYOffset, 10); + html.pageYOffset = start + 1; + end = parseInt(html.pageYOffset, 10); + html.pageYOffset = start; + if (end > start) { + return html; + } else { + return document.body; + } + }; + + ZammadChat.prototype.render = function() { + if (!this.el || !document.querySelector('.zammad-chat')) { + this.renderBase(); + } + document.querySelector("." + this.options.buttonClass).classList.add(this.inactiveClass); + this.setAgentOnlineState('online'); + this.log.debug('widget rendered'); + this.startTimeoutObservers(); + this.idleTimeout.start(); + this.sessionId = sessionStorage.getItem('sessionId'); + return this.send('chat_status_customer', { + session_id: this.sessionId, + url: window.location.href + }); + }; + + ZammadChat.prototype.renderBase = function() { + if (this.el) { + this.el.remove(); + } + this.options.target.innerHTML += this.view('chat')({ + title: this.options.title, + scrollHint: this.options.scrollHint + }); + this.el = this.options.target.querySelector('.zammad-chat'); + this.input = this.el.querySelector('.zammad-chat-input'); + this.body = this.el.querySelector('.zammad-chat-body'); + this.el.querySelector('.js-chat-open').addEventListener('click', this.open); + this.el.querySelector('.js-chat-toggle').addEventListener('click', this.toggle); + this.el.querySelector('.js-chat-status').addEventListener('click', this.stopPropagation); + this.el.querySelector('.zammad-chat-controls').addEventListener('submit', this.onSubmit); + this.body.addEventListener('scroll', this.detectScrolledtoBottom); + this.el.querySelector('.zammad-scroll-hint').addEventListener('click', this.onScrollHintClick); + this.input.addEventListener('keydown', this.onKeydown); + this.input.addEventListener('input', this.onInput); + this.input.addEventListener('paste', this.onPaste); + this.input.addEventListener('drop', this.onDrop); + window.addEventListener('beforeunload', this.onLeaveTemporary); + return window.addEventListener('hashchange', (function(_this) { + return function() { + if (_this.isOpen) { + if (_this.sessionId) { + _this.send('chat_session_notice', { + session_id: _this.sessionId, + message: window.location.href + }); + } + return; + } + return _this.idleTimeout.start(); + }; + })(this)); + }; + + ZammadChat.prototype.stopPropagation = function(event) { + return event.stopPropagation(); + }; + + ZammadChat.prototype.onPaste = function(e) { + var dataTransfer, file, reader, x, y; + e.stopPropagation(); + e.preventDefault(); + if (window.dataTransfer) { + dataTransfer = window.dataTransfer; + } else if (e.dataTransfer) { + dataTransfer = e.dataTransfer; + } else { + throw 'No clipboardData support'; + } + x = e.clientX; + y = e.clientY; + file = dataTransfer.files[0]; + if (file.type.match('image.*')) { + reader = new FileReader(); + reader.onload = (function(_this) { + return function(e) { + var insert; + insert = function(dataUrl, width) { + var img, pos, range, result; + if (_this.isRetina()) { + width = width / 2; + } + result = dataUrl; + img = new Image(); + img.style.width = '100%'; + img.style.maxWidth = width(+'px'); + img.src = result; + if (document.caretPositionFromPoint) { + pos = document.caretPositionFromPoint(x, y); + range = document.createRange(); + range.setStart(pos.offsetNode, pos.offset); + range.collapse(); + return range.insertNode(img); + } else if (document.caretRangeFromPoint) { + range = document.caretRangeFromPoint(x, y); + return range.insertNode(img); + } else { + return console.log('could not find carat'); + } + }; + return _this.resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert); + }; + })(this); + return reader.readAsDataURL(file); + } + }; + + ZammadChat.prototype.onPaste = function(e) { + var clipboardData, docType, html, htmlTmp, imageFile, imageInserted, item, j, k, l, len, len1, len2, len3, m, match, newTag, node, outer, reader, ref, ref1, ref2, ref3, regex, replacementTag, text; + e.stopPropagation(); + e.preventDefault(); + if (e.clipboardData) { + clipboardData = e.clipboardData; + } else if (window.clipboardData) { + clipboardData = window.clipboardData; + } else if (e.clipboardData) { + clipboardData = e.clipboardData; + } else { + throw 'No clipboardData support'; + } + imageInserted = false; + if (clipboardData && clipboardData.items && clipboardData.items[0]) { + item = clipboardData.items[0]; + if (item.kind === 'file' && (item.type === 'image/png' || item.type === 'image/jpeg')) { + imageFile = item.getAsFile(); + reader = new FileReader(); + reader.onload = (function(_this) { + return function(e) { + var insert; + insert = function(dataUrl, width) { + var img; + if (_this.isRetina()) { + width = width / 2; + } + img = new Image(); + img.style.width = '100%'; + img.style.maxWidth = width(+'px'); + img.src = dataUrl; + return document.execCommand('insertHTML', false, img); + }; + return _this.resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert); + }; + })(this); + reader.readAsDataURL(imageFile); + imageInserted = true; + } + } + if (imageInserted) { + return; + } + text = void 0; + docType = void 0; + try { + text = clipboardData.getData('text/html'); + docType = 'html'; + if (!text || text.length === 0) { + docType = 'text'; + text = clipboardData.getData('text/plain'); + } + if (!text || text.length === 0) { + docType = 'text2'; + text = clipboardData.getData('text'); + } + } catch (error) { + e = error; + console.log('Sorry, can\'t insert markup because browser is not supporting it.'); + docType = 'text3'; + text = clipboardData.getData('text'); + } + if (docType === 'text' || docType === 'text2' || docType === 'text3') { + text = '
          ' + text.replace(/\n/g, '
          ') + '
          '; + text = text.replace(/
          <\/div>/g, '

          '); + } + console.log('p', docType, text); + if (docType === 'html') { + html = document.createElement('div'); + html.innerHTML = text; + match = false; + htmlTmp = text; + regex = new RegExp('<(/w|w)\:[A-Za-z]'); + if (htmlTmp.match(regex)) { + match = true; + htmlTmp = htmlTmp.replace(regex, ''); + } + regex = new RegExp('<(/o|o)\:[A-Za-z]'); + if (htmlTmp.match(regex)) { + match = true; + htmlTmp = htmlTmp.replace(regex, ''); + } + if (match) { + html = this.wordFilter(html); + } + ref = html.childNodes; + for (j = 0, len = ref.length; j < len; j++) { + node = ref[j]; + if (node.nodeType === 8) { + node.remove(); + } + } + ref1 = html.querySelectorAll('a, font, small, time, form, label'); + for (k = 0, len1 = ref1.length; k < len1; k++) { + node = ref1[k]; + node.outerHTML = node.innerHTML; + } + replacementTag = 'div'; + ref2 = html.querySelectorAll('textarea'); + for (l = 0, len2 = ref2.length; l < len2; l++) { + node = ref2[l]; + outer = node.outerHTML; + regex = new RegExp('<' + node.tagName, 'i'); + newTag = outer.replace(regex, '<' + replacementTag); + regex = new RegExp(' new Date(new Date().getTime() - 1500)) { + return; + } + this.isTyping = new Date(); + this.send('chat_session_typing', { + session_id: this.sessionId + }); + return this.inactiveTimeout.start(); + }; + + ZammadChat.prototype.onSubmit = function(event) { + event.preventDefault(); + return this.sendMessage(); + }; + + ZammadChat.prototype.sendMessage = function() { + var message, messageElement; + message = this.input.innerHTML; + if (!message) { + return; + } + this.inactiveTimeout.start(); + sessionStorage.removeItem('unfinished_message'); + messageElement = this.view('message')({ + message: message, + from: 'customer', + id: this._messageCount++, + unreadClass: '' + }); + this.maybeAddTimestamp(); + if (this.el.querySelector('.zammad-chat-message--typing')) { + this.lastAddedType = 'typing-placeholder'; + this.el.querySelector('.zammad-chat-message--typing').insertAdjacentHTML('beforebegin', messageElement); + } else { + this.lastAddedType = 'message--customer'; + this.body.insertAdjacentHTML('beforeend', messageElement); + } + this.input.innerHTML = ''; + this.scrollToBottom(); + return this.send('chat_session_message', { + content: message, + id: this._messageCount, + session_id: this.sessionId + }); + }; + + ZammadChat.prototype.receiveMessage = function(data) { + this.inactiveTimeout.start(); + this.onAgentTypingEnd(); + this.maybeAddTimestamp(); + this.renderMessage({ + message: data.message.content, + id: data.id, + from: 'agent' + }); + return this.scrollToBottom({ + showHint: true + }); + }; + + ZammadChat.prototype.renderMessage = function(data) { + this.lastAddedType = "message--" + data.from; + data.unreadClass = document.hidden ? ' zammad-chat-message--unread' : ''; + return this.body.insertAdjacentHTML('beforeend', this.view('message')(data)); + }; + + ZammadChat.prototype.open = function() { + var remainerHeight; + if (this.isOpen) { + this.log.debug('widget already open, block'); + return; + } + this.isOpen = true; + this.log.debug('open widget'); + this.show(); + if (!this.sessionId) { + this.showLoader(); + } + this.el.classList.add('zammad-chat-is-open'); + remainerHeight = this.el.clientHeight - this.el.querySelector('.zammad-chat-header').offsetHeight; + this.el.style.transform = "translateY(" + remainerHeight + "px)"; + this.el.clientHeight; + if (!this.sessionId) { + this.el.addEventListener('transitionend', this.onOpenTransitionend); + this.el.classList.add('zammad-chat--animate'); + this.el.clientHeight; + this.el.style.transform = ''; + return this.send('chat_session_init', { + url: window.location.href + }); + } else { + this.el.style.transform = ''; + return this.onOpenTransitionend(); + } + }; + + ZammadChat.prototype.onOpenTransitionend = function() { + this.el.removeEventListener('transitionend', this.onOpenTransitionend); + this.el.classList.remove('zammad-chat--animate'); + this.idleTimeout.stop(); + if (this.isFullscreen) { + return this.disableScrollOnRoot(); + } + }; + + ZammadChat.prototype.sessionClose = function() { + this.send('chat_session_close', { + session_id: this.sessionId + }); + this.inactiveTimeout.stop(); + this.waitingListTimeout.stop(); + sessionStorage.removeItem('unfinished_message'); + if (this.onInitialQueueDelayId) { + clearTimeout(this.onInitialQueueDelayId); + } + return this.setSessionId(void 0); + }; + + ZammadChat.prototype.toggle = function(event) { + if (this.isOpen) { + return this.close(event); + } else { + return this.open(event); + } + }; + + ZammadChat.prototype.close = function(event) { + var remainerHeight; + if (!this.isOpen) { + this.log.debug('can\'t close widget, it\'s not open'); + return; + } + if (this.initDelayId) { + clearTimeout(this.initDelayId); + } + if (!this.sessionId) { + this.log.debug('can\'t close widget without sessionId'); + return; + } + this.log.debug('close widget'); + if (event) { + event.stopPropagation(); + } + this.sessionClose(); + if (this.isFullscreen) { + this.enableScrollOnRoot(); + } + remainerHeight = this.el.clientHeight - this.el.querySelector('.zammad-chat-header').offsetHeight; + this.el.addEventListener('transitionend', this.onCloseTransitionend); + this.el.classList.add('zammad-chat--animate'); + document.offsetHeight; + return this.el.style.transform = "translateY(" + remainerHeight + "px)"; + }; + + ZammadChat.prototype.onCloseTransitionend = function() { + this.el.removeEventListener('transitionend', this.onCloseTransitionend); + this.el.classList.remove('zammad-chat-is-open', 'zammad-chat--animate'); + this.el.style.transform = ''; + this.showLoader(); + this.el.querySelector('.zammad-chat-welcome').classList.remove('zammad-chat-is-hidden'); + this.el.querySelector('.zammad-chat-agent').classList.add('zammad-chat-is-hidden'); + this.el.querySelector('.zammad-chat-agent-status').classList.add('zammad-chat-is-hidden'); + this.isOpen = false; + return this.io.reconnect(); + }; + + ZammadChat.prototype.onWebSocketClose = function() { + if (this.isOpen) { + return; + } + if (this.el) { + this.el.classList.remove('zammad-chat-is-shown'); + return this.el.classList.remove('zammad-chat-is-loaded'); + } + }; + + ZammadChat.prototype.show = function() { + if (this.state === 'offline') { + return; + } + this.el.classList.add('zammad-chat-is-loaded'); + return this.el.classList.add('zammad-chat-is-shown'); + }; + + ZammadChat.prototype.disableInput = function() { + this.input.disabled = true; + return this.el.querySelector('.zammad-chat-send').disabled = true; + }; + + ZammadChat.prototype.enableInput = function() { + this.input.disabled = false; + return this.el.querySelector('.zammad-chat-send').disabled = false; + }; + + ZammadChat.prototype.hideModal = function() { + return this.el.querySelector('.zammad-chat-modal').innerHTML = ''; + }; + + ZammadChat.prototype.onQueueScreen = function(data) { + var show; + this.setSessionId(data.session_id); + show = (function(_this) { + return function() { + _this.onQueue(data); + return _this.waitingListTimeout.start(); + }; + })(this); + if (this.initialQueueDelay && !this.onInitialQueueDelayId) { + this.onInitialQueueDelayId = setTimeout(show, this.initialQueueDelay); + return; + } + if (this.onInitialQueueDelayId) { + clearTimeout(this.onInitialQueueDelayId); + } + return show(); + }; + + ZammadChat.prototype.onQueue = function(data) { + this.log.notice('onQueue', data.position); + this.inQueue = true; + return this.el.querySelector('.zammad-chat-modal').innerHTML = this.view('waiting')({ + position: data.position + }); + }; + + ZammadChat.prototype.onAgentTypingStart = function() { + if (this.stopTypingId) { + clearTimeout(this.stopTypingId); + } + this.stopTypingId = setTimeout(this.onAgentTypingEnd, 3000); + if (this.el.querySelector('.zammad-chat-message--typing')) { + return; + } + this.maybeAddTimestamp(); + this.body.insertAdjacentHTML('beforeend', this.view('typingIndicator')()); + if (!this.isVisible(this.el.querySelector('.zammad-chat-message--typing'), true)) { + return; + } + return this.scrollToBottom(); + }; + + ZammadChat.prototype.onAgentTypingEnd = function() { + if (this.el.querySelector('.zammad-chat-message--typing')) { + return this.el.querySelector('.zammad-chat-message--typing').remove(); + } + }; + + ZammadChat.prototype.onLeaveTemporary = function() { + if (!this.sessionId) { + return; + } + return this.send('chat_session_leave_temporary', { + session_id: this.sessionId + }); + }; + + ZammadChat.prototype.maybeAddTimestamp = function() { + var label, time, timestamp; + timestamp = Date.now(); + if (!this.lastTimestamp || (timestamp - this.lastTimestamp) > this.showTimeEveryXMinutes * 60000) { + label = this.T('Today'); + time = new Date().toTimeString().substr(0, 5); + if (this.lastAddedType === 'timestamp') { + this.updateLastTimestamp(label, time); + return this.lastTimestamp = timestamp; + } else { + this.body.insertAdjacentHTML('beforeend', this.view('timestamp')({ + label: label, + time: time + })); + this.lastTimestamp = timestamp; + this.lastAddedType = 'timestamp'; + return this.scrollToBottom(); + } + } + }; + + ZammadChat.prototype.updateLastTimestamp = function(label, time) { + var timestamps; + if (!this.el) { + return; + } + timestamps = this.el.querySelectorAll('.zammad-chat-body .zammad-chat-timestamp'); + if (!timestamps) { + return; + } + return timestamps[timestamps.length - 1].outerHTML = this.view('timestamp')({ + label: label, + time: time + }); + }; + + ZammadChat.prototype.addStatus = function(status) { + if (!this.el) { + return; + } + this.maybeAddTimestamp(); + this.body.insertAdjacentHTML('beforeend', this.view('status')({ + status: status + })); + return this.scrollToBottom(); + }; + + ZammadChat.prototype.detectScrolledtoBottom = function() { + var scrollBottom; + scrollBottom = this.body.scrollTop + this.body.offsetHeight; + this.scrolledToBottom = Math.abs(scrollBottom - this.body.scrollHeight) <= this.scrollSnapTolerance; + if (this.scrolledToBottom) { + return this.el.querySelector('.zammad-scroll-hint').classList.add('is-hidden'); + } + }; + + ZammadChat.prototype.showScrollHint = function() { + this.el.querySelector('.zammad-scroll-hint').classList.remove('is-hidden'); + return this.body.scrollTop = this.body.scrollTop + this.el.querySelector('.zammad-scroll-hint').offsetHeight; + }; + + ZammadChat.prototype.onScrollHintClick = function() { + return this.body.scrollTo({ + top: this.body.scrollHeight, + behavior: 'smooth' + }); + }; + + ZammadChat.prototype.scrollToBottom = function(arg) { + var showHint; + showHint = (arg != null ? arg : { + showHint: false + }).showHint; + if (this.scrolledToBottom) { + return this.body.scrollTop = this.body.scrollHeight; + } else if (showHint) { + return this.showScrollHint(); + } + }; + + ZammadChat.prototype.destroy = function(params) { + if (params == null) { + params = {}; + } + this.log.debug('destroy widget', params); + this.setAgentOnlineState('offline'); + if (params.remove && this.el) { + this.el.remove(); + } + if (this.waitingListTimeout) { + this.waitingListTimeout.stop(); + } + if (this.inactiveTimeout) { + this.inactiveTimeout.stop(); + } + if (this.idleTimeout) { + this.idleTimeout.stop(); + } + return this.io.close(); + }; + + ZammadChat.prototype.reconnect = function() { + this.log.notice('reconnecting'); + this.disableInput(); + this.lastAddedType = 'status'; + this.setAgentOnlineState('connecting'); + return this.addStatus(this.T('Connection lost')); + }; + + ZammadChat.prototype.onConnectionReestablished = function() { + this.lastAddedType = 'status'; + this.setAgentOnlineState('online'); + return this.addStatus(this.T('Connection re-established')); + }; + + ZammadChat.prototype.onSessionClosed = function(data) { + this.addStatus(this.T('Chat closed by %s', data.realname)); + this.disableInput(); + this.setAgentOnlineState('offline'); + return this.inactiveTimeout.stop(); + }; + + ZammadChat.prototype.setSessionId = function(id) { + this.sessionId = id; + if (id === void 0) { + return sessionStorage.removeItem('sessionId'); + } else { + return sessionStorage.setItem('sessionId', id); + } + }; + + ZammadChat.prototype.onConnectionEstablished = function(data) { + if (this.onInitialQueueDelayId) { + clearTimeout(this.onInitialQueueDelayId); + } + this.inQueue = false; + if (data.agent) { + this.agent = data.agent; + } + if (data.session_id) { + this.setSessionId(data.session_id); + } + this.body.innerHTML = ''; + this.el.querySelector('.zammad-chat-agent').innerHTML = this.view('agent')({ + agent: this.agent + }); + this.enableInput(); + this.hideModal(); + this.el.querySelector('.zammad-chat-welcome').classList.add('zammad-chat-is-hidden'); + this.el.querySelector('.zammad-chat-agent').classList.remove('zammad-chat-is-hidden'); + this.el.querySelector('.zammad-chat-agent-status').classList.remove('zammad-chat-is-hidden'); + if (!this.isFullscreen) { + this.input.focus(); + } + this.setAgentOnlineState('online'); + this.waitingListTimeout.stop(); + this.idleTimeout.stop(); + return this.inactiveTimeout.start(); + }; + + ZammadChat.prototype.showCustomerTimeout = function() { + this.el.querySelector('.zammad-chat-modal').innerHTML = this.view('customer_timeout')({ + agent: this.agent.name, + delay: this.options.inactiveTimeout + }); + this.el.querySelector('.js-restart').addEventListener('click', function() { + return location.reload(); + }); + return this.sessionClose(); + }; + + ZammadChat.prototype.showWaitingListTimeout = function() { + this.el.querySelector('.zammad-chat-modal').innerHTML = this.view('waiting_list_timeout')({ + delay: this.options.watingListTimeout + }); + this.el.querySelector('.js-restart').addEventListener('click', function() { + return location.reload(); + }); + return this.sessionClose(); + }; + + ZammadChat.prototype.showLoader = function() { + return this.el.querySelector('.zammad-chat-modal').innerHTML = this.view('loader')(); + }; + + ZammadChat.prototype.setAgentOnlineState = function(state) { + var capitalizedState; + this.state = state; + if (!this.el) { + return; + } + capitalizedState = state.charAt(0).toUpperCase() + state.slice(1); + this.el.querySelector('.zammad-chat-agent-status').dataset.status = state; + return this.el.querySelector('.zammad-chat-agent-status').textContent = this.T(capitalizedState); + }; + + ZammadChat.prototype.detectHost = function() { + var protocol; + protocol = 'ws://'; + if (scriptProtocol === 'https') { + protocol = 'wss://'; + } + return this.options.host = "" + protocol + scriptHost + "/ws"; + }; + + ZammadChat.prototype.loadCss = function() { + var newSS, styles, url; + if (!this.options.cssAutoload) { + return; + } + url = this.options.cssUrl; + if (!url) { + url = this.options.host.replace(/^wss/i, 'https').replace(/^ws/i, 'http').replace(/\/ws/i, ''); + url += '/assets/chat/chat.css'; + } + this.log.debug("load css from '" + url + "'"); + styles = "@import url('" + url + "');"; + newSS = document.createElement('link'); + newSS.onload = this.onCssLoaded; + newSS.rel = 'stylesheet'; + newSS.href = 'data:text/css,' + escape(styles); + return document.getElementsByTagName('head')[0].appendChild(newSS); + }; + + ZammadChat.prototype.onCssLoaded = function() { + this.cssLoaded = true; + if (this.socketReady) { + return this.onReady(); + } + }; + + ZammadChat.prototype.startTimeoutObservers = function() { + this.idleTimeout = new Timeout({ + logPrefix: 'idleTimeout', + debug: this.options.debug, + timeout: this.options.idleTimeout, + timeoutIntervallCheck: this.options.idleTimeoutIntervallCheck, + callback: (function(_this) { + return function() { + _this.log.debug('Idle timeout reached, hide widget', new Date); + return _this.destroy({ + remove: true + }); + }; + })(this) + }); + this.inactiveTimeout = new Timeout({ + logPrefix: 'inactiveTimeout', + debug: this.options.debug, + timeout: this.options.inactiveTimeout, + timeoutIntervallCheck: this.options.inactiveTimeoutIntervallCheck, + callback: (function(_this) { + return function() { + _this.log.debug('Inactive timeout reached, show timeout screen.', new Date); + _this.showCustomerTimeout(); + return _this.destroy({ + remove: false + }); + }; + })(this) + }); + return this.waitingListTimeout = new Timeout({ + logPrefix: 'waitingListTimeout', + debug: this.options.debug, + timeout: this.options.waitingListTimeout, + timeoutIntervallCheck: this.options.waitingListTimeoutIntervallCheck, + callback: (function(_this) { + return function() { + _this.log.debug('Waiting list timeout reached, show timeout screen.', new Date); + _this.showWaitingListTimeout(); + return _this.destroy({ + remove: false + }); + }; + })(this) + }); + }; + + ZammadChat.prototype.disableScrollOnRoot = function() { + this.rootScrollOffset = this.scrollRoot.scrollTop; + this.scrollRoot.style.overflow = 'hidden'; + return this.scrollRoot.style.position = 'fixed'; + }; + + ZammadChat.prototype.enableScrollOnRoot = function() { + this.scrollRoot.scrollTop = this.rootScrollOffset(+'px'); + this.scrollRoot.style.overflow = ''; + return this.scrollRoot.style.position = ''; + }; + + ZammadChat.prototype.isVisible = function(el, partial, hidden, direction) { + var bViz, clientSize, hVisible, lViz, rViz, rec, tViz, vVisible, vpHeight, vpWidth; + if (el.length < 1) { + return; + } + vpWidth = window.innerWidth; + vpHeight = window.innerHeight; + direction = direction ? direction : 'both'; + clientSize = hidden === true ? t.offsetWidth * t.offsetHeight : true; + rec = el.getBoundingClientRect(); + tViz = rec.top >= 0 && rec.top < vpHeight; + bViz = rec.bottom > 0 && rec.bottom <= vpHeight; + lViz = rec.left >= 0 && rec.left < vpWidth; + rViz = rec.right > 0 && rec.right <= vpWidth; + vVisible = partial ? tViz || bViz : tViz && bViz; + hVisible = partial ? lViz || rViz : lViz && rViz; + if (direction === 'both') { + return clientSize && vVisible && hVisible; + } else if (direction === 'vertical') { + return clientSize && vVisible; + } else if (direction === 'horizontal') { + return clientSize && hVisible; + } + }; + + ZammadChat.prototype.isRetina = function() { + var mq; + if (window.matchMedia) { + mq = window.matchMedia('only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)'); + return mq && mq.matches || (window.devicePixelRatio > 1); + } + return false; + }; + + ZammadChat.prototype.resizeImage = function(dataURL, x, y, sizeFactor, type, quallity, callback, force) { + var imageObject; + if (x == null) { + x = 'auto'; + } + if (y == null) { + y = 'auto'; + } + if (sizeFactor == null) { + sizeFactor = 1; + } + if (force == null) { + force = true; + } + imageObject = new Image(); + imageObject.onload = function() { + var canvas, context, factor, imageHeight, imageWidth, newDataUrl, resize; + imageWidth = imageObject.width; + imageHeight = imageObject.height; + console.log('ImageService', 'current size', imageWidth, imageHeight); + if (y === 'auto' && x === 'auto') { + x = imageWidth; + y = imageHeight; + } + if (y === 'auto') { + factor = imageWidth / x; + y = imageHeight / factor; + } + if (x === 'auto') { + factor = imageWidth / y; + x = imageHeight / factor; + } + resize = false; + if (x < imageWidth || y < imageHeight) { + resize = true; + x = x * sizeFactor; + y = y * sizeFactor; + } else { + x = imageWidth; + y = imageHeight; + } + canvas = document.createElement('canvas'); + canvas.width = x; + canvas.height = y; + context = canvas.getContext('2d'); + context.drawImage(imageObject, 0, 0, x, y); + if (quallity === 'auto') { + if (x < 200 && y < 200) { + quallity = 1; + } else if (x < 400 && y < 400) { + quallity = 0.9; + } else if (x < 600 && y < 600) { + quallity = 0.8; + } else if (x < 900 && y < 900) { + quallity = 0.7; + } else { + quallity = 0.6; + } + } + newDataUrl = canvas.toDataURL(type, quallity); + if (resize) { + console.log('ImageService', 'resize', x / sizeFactor, y / sizeFactor, quallity, (newDataUrl.length * 0.75) / 1024 / 1024, 'in mb'); + callback(newDataUrl, x / sizeFactor, y / sizeFactor, true); + return; + } + console.log('ImageService', 'no resize', x, y, quallity, (newDataUrl.length * 0.75) / 1024 / 1024, 'in mb'); + return callback(newDataUrl, x, y, false); + }; + return imageObject.src = dataURL; + }; + + ZammadChat.prototype.pasteHtmlAtCaret = function(html) { + var el, frag, lastNode, node, range, sel; + sel = void 0; + range = void 0; + if (window.getSelection) { + sel = window.getSelection(); + if (sel.getRangeAt && sel.rangeCount) { + range = sel.getRangeAt(0); + range.deleteContents(); + el = document.createElement('div'); + el.innerHTML = html; + frag = document.createDocumentFragment(node, lastNode); + while (node = el.firstChild) { + lastNode = frag.appendChild(node); + } + range.insertNode(frag); + if (lastNode) { + range = range.cloneRange(); + range.setStartAfter(lastNode); + range.collapse(true); + sel.removeAllRanges(); + return sel.addRange(range); + } + } + } else if (document.selection && document.selection.type !== 'Control') { + return document.selection.createRange().pasteHTML(html); + } + }; + + ZammadChat.prototype.wordFilter = function(editor) { + var content, cur_level, el, i, j, k, l, last_level, len, len1, len2, len3, len4, len5, len6, len7, list_tag, m, matches, n, o, p, pnt, q, r, ref, ref1, ref10, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, s, start, str, txt; + content = editor.html(); + content = content.replace(//gi, ''); + content = content.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, ''); + content = content.replace(/<(\/?)s>/gi, '<$1strike>'); + content = content.replace(/ /gi, ' '); + editor.innerHTML = content; + ref = editor.querySelectorAll('p'); + for (j = 0, len = ref.length; j < len; j++) { + p = ref[j]; + str = p.getAttribute('style'); + matches = /mso-list:\w+ \w+([0-9]+)/.exec(str); + if (matches) { + p.dataset._listLevel = parseInt(matches[1], 10); + } + } + last_level = 0; + pnt = null; + ref1 = editor.querySelectorAll('p'); + for (k = 0, len1 = ref1.length; k < len1; k++) { + p = ref1[k]; + cur_level = p.dataset._listLevel; + if (cur_level !== void 0) { + txt = p.textContent; + list_tag = '
            '; + if (/^\s*\w+\./.test(txt)) { + matches = /([0-9])\./.exec(txt); + if (matches) { + start = parseInt(matches[1], 10); + list_tag = (ref2 = start > 1) != null ? ref2 : '
              ': '
                ' + }; + } else { + list_tag = '
                  '; + } + } + if (cur_level > last_level) { + if (last_level === 0) { + p.insertAdjacentHTML('beforebegin', list_tag); + pnt = p.previousElementSibling; + } else { + + } + pnt.insertAdjacentHTML('beforeend', list_tag); + } + if (cur_level < last_level) { + for (i = l = ref3 = i, ref4 = last_level - cur_level; ref3 <= ref4 ? l <= ref4 : l >= ref4; i = ref3 <= ref4 ? ++l : --l) { + pnt = pnt.parentNode; + } + } + if (p.querySelector('span:first')) { + p.querySelector('span:first').remove(); + } + pnt.insertAdjacentHTML('beforeend', '
                1. ' + p.innerHTML + '
                2. '); + p.remove(); + last_level = cur_level; + } else { + last_level = 0; + } + } + ref5 = editor.querySelectorAll('[style]'); + for (m = 0, len2 = ref5.length; m < len2; m++) { + el = ref5[m]; + el.removeAttribute('style'); + } + ref6 = editor.querySelectorAll('[align]'); + for (n = 0, len3 = ref6.length; n < len3; n++) { + el = ref6[n]; + el.removeAttribute('align'); + } + ref7 = editor.querySelectorAll('span'); + for (o = 0, len4 = ref7.length; o < len4; o++) { + el = ref7[o]; + el.outerHTML = el.innerHTML; + } + ref8 = editor.querySelectorAll('span:empty'); + for (q = 0, len5 = ref8.length; q < len5; q++) { + el = ref8[q]; + el.remove(); + } + ref9 = editor.querySelectorAll("[class^='Mso']"); + for (r = 0, len6 = ref9.length; r < len6; r++) { + el = ref9[r]; + el.removeAttribute('class'); + } + ref10 = editor.querySelectorAll('p:empty'); + for (s = 0, len7 = ref10.length; s < len7; s++) { + el = ref10[s]; + el.remove(); + } + return editor; + }; + + ZammadChat.prototype.removeAttribute = function(element) { + var att, j, len, ref, results; + if (!element) { + return; + } + ref = element.attributes; + results = []; + for (j = 0, len = ref.length; j < len; j++) { + att = ref[j]; + results.push(element.removeAttribute(att.name)); + } + return results; + }; + + ZammadChat.prototype.removeAttributes = function(html) { + var j, len, node, ref; + ref = html.querySelectorAll('*'); + for (j = 0, len = ref.length; j < len; j++) { + node = ref[j]; + this.removeAttribute(node); + } + return html; + }; + + return ZammadChat; + + })(Base); + return window.ZammadChat = ZammadChat; +})(window); + +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["chat"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + __out.push('
                  \n
                  \n
                  \n \n \n \n \n \n
                  \n
                  \n
                  \n
                  \n \n '); + + __out.push(this.T(this.title)); + + __out.push('\n
                  \n
                  \n
                  \n \n
                  \n
                  \n
                  \n \n
                  \n
                  '); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["customer_timeout"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + __out.push('
                  \n '); + + if (this.agent) { + __out.push('\n '); + __out.push(this.T('Since you didn\'t respond in the last %s minutes your conversation with %s got closed.', this.delay, this.agent)); + __out.push('\n '); + } else { + __out.push('\n '); + __out.push(this.T('Since you didn\'t respond in the last %s minutes your conversation got closed.', this.delay)); + __out.push('\n '); + } + + __out.push('\n
                  \n
                  '); + + __out.push(this.T('Start new conversation')); + + __out.push('
                  \n
                  '); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["loader"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + __out.push('\n \n \n \n\n'); + + __out.push(this.T('Connecting')); + + __out.push(''); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["message"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + __out.push('
                  \n '); + + __out.push(this.message); + + __out.push('\n
                  '); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["status"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + __out.push('
                  \n
                  \n '); + + __out.push(this.status); + + __out.push('\n
                  \n
                  '); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["timestamp"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + __out.push('
                  '); + + __out.push(__sanitize(this.label)); + + __out.push(' '); + + __out.push(__sanitize(this.time)); + + __out.push('
                  '); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["typingIndicator"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + __out.push('
                  \n \n \n \n \n \n \n \n
                  '); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["waiting"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + __out.push('
                  \n \n \n \n \n \n '); + + __out.push(this.T('All colleagues are busy.')); + + __out.push('
                  \n '); + + __out.push(this.T('You are on waiting list position %s.', this.position)); + + __out.push('\n
                  '); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["waiting_list_timeout"] = function (__obj) { + if (!__obj) __obj = {}; + var __out = [], __capture = function(callback) { + var out = __out, result; + __out = []; + callback.call(this); + result = __out.join(''); + __out = out; + return __safe(result); + }, __sanitize = function(value) { + if (value && value.ecoSafe) { + return value; + } else if (typeof value !== 'undefined' && value != null) { + return __escape(value); + } else { + return ''; + } + }, __safe, __objSafe = __obj.safe, __escape = __obj.escape; + __safe = __obj.safe = function(value) { + if (value && value.ecoSafe) { + return value; + } else { + if (!(typeof value !== 'undefined' && value != null)) value = ''; + var result = new String(value); + result.ecoSafe = true; + return result; + } + }; + if (!__escape) { + __escape = __obj.escape = function(value) { + return ('' + value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }; + } + (function() { + (function() { + __out.push('
                  \n '); + + __out.push(this.T('We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!')); + + __out.push('\n
                  \n
                  '); + + __out.push(this.T('Start new conversation')); + + __out.push('
                  \n
                  '); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; diff --git a/public/assets/chat/chat-no-jquery.min.js b/public/assets/chat/chat-no-jquery.min.js new file mode 100644 index 000000000..079c0cbcc --- /dev/null +++ b/public/assets/chat/chat-no-jquery.min.js @@ -0,0 +1,2 @@ +window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(e){e||(e={});var t,s=[],n=function(e){return e&&e.ecoSafe?e:"undefined"!=typeof e&&null!=e?i(e):""},o=e.safe,i=e.escape;return t=e.safe=function(e){if(e&&e.ecoSafe)return e;"undefined"!=typeof e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},i||(i=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(s.push('\n\n')),s.push('\n\n '),s.push(n(this.agent.name)),s.push("\n")}).call(this)}.call(e),e.safe=o,e.escape=i,s.join("")};var extend=function(e,t){function s(){this.constructor=e}for(var n in t)hasProp.call(t,n)&&(e[n]=t[n]);return s.prototype=t.prototype,e.prototype=new s,e.__super__=t.prototype,e},hasProp={}.hasOwnProperty,bind=function(e,t){return function(){return e.apply(t,arguments)}},slice=[].slice;!function(e){var s,n,o,i,a,r,l,c,d,h;return h=document.getElementsByTagName("script"),l=h[h.length-1],d=e.location.protocol.replace(":",""),l&&l.src&&(c=l.src.match(".*://([^:/]*).*")[1],d=l.src.match("(.*)://[^:/]*.*")[1]),n=function(){function e(e){var t,s,n;this.options={},s=this.defaults;for(t in s)n=s[t],this.options[t]=n;for(t in e)n=e[t],this.options[t]=n}return e.prototype.defaults={debug:!1},e}(),s=function(e){function t(e){t.__super__.constructor.call(this,e),this.log=new i({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return extend(t,e),t}(n),i=function(e){function t(){return this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),t.__super__.constructor.apply(this,arguments)}return extend(t,e),t.prototype.debug=function(){var e;if(e=1<=arguments.length?slice.call(arguments,0):[],this.options.debug)return this.log("debug",e)},t.prototype.notice=function(){var e;return e=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",e)},t.prototype.error=function(){var e;return e=1<=arguments.length?slice.call(arguments,0):[],this.log("error",e)},t.prototype.log=function(e,t){var s,n,o,i,a;if(t.unshift("||"),t.unshift(e),t.unshift(this.options.logPrefix),console.log.apply(console,t),this.options.debug){for(a="",o=0,i=t.length;o"+a+"
                  "+s.innerHTML:void 0}},t}(n),a=function(e){function t(){return this.stop=bind(this.stop,this),this.start=bind(this.start,this),t.__super__.constructor.apply(this,arguments)}return extend(t,e),t.prototype.timeoutStartedAt=null,t.prototype.logPrefix="timeout",t.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},t.prototype.start=function(){var e,t;return this.stop(),t=new Date,e=function(e){return function(){var s;if(s=new Date-new Date(t.getTime()+1e3*e.options.timeout*60),e.log.debug("Timeout check for "+e.options.timeout+" minutes (left "+s/1e3+" sec.)"),!(s<0))return e.stop(),e.options.callback()}}(this),this.log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(e,1e3*this.options.timeoutIntervallCheck*60)},t.prototype.stop=function(){if(this.intervallId)return this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)},t}(s),o=function(t){function s(){return this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),s.__super__.constructor.apply(this,arguments)}return extend(s,t),s.prototype.logPrefix="io",s.prototype.set=function(e){var t,s,n;s=[];for(t in e)n=e[t],s.push(this.options[t]=n);return s},s.prototype.connect=function(){return this.log.debug("Connecting to "+this.options.host),this.ws=new e.WebSocket(""+this.options.host),this.ws.onopen=function(e){return function(t){return e.log.debug("onOpen",t),e.options.onOpen(t),e.ping()}}(this),this.ws.onmessage=function(e){return function(t){var s,n,o,i;for(i=JSON.parse(t.data),e.log.debug("onMessage",t.data),s=0,n=i.length;sChat with us!",scrollHint:"Scroll down to see new messages",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5},n.prototype.logPrefix="chat",n.prototype._messageCount=0,n.prototype.isOpen=!1,n.prototype.blinkOnlineInterval=null,n.prototype.stopBlinOnlineStateTimeout=null,n.prototype.showTimeEveryXMinutes=2,n.prototype.lastTimestamp=null,n.prototype.lastAddedType=null,n.prototype.inputTimeout=null,n.prototype.isTyping=!1,n.prototype.state="offline",n.prototype.initialQueueDelay=1e4,n.prototype.translations={de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Chat closed by %s":"Chat beendet von %s","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!"},es:{"Chat with us!":"Chatee con nosotros!","Scroll down to see new messages":"Haga scroll hacia abajo para ver nuevos mensajes",Online:"En linea",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexión restablecida",Today:"Hoy",Send:"Enviar","Chat closed by %s":"Chat cerrado por %s","Compose your message...":"Escriba su mensaje...","All colleagues are busy.":"Todos los agentes están ocupados.","You are on waiting list position %s.":"Usted está en la posición %s de la lista de espera.","Start new conversation":"Iniciar nueva conversación","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.","Since you didn't respond in the last %s minutes your conversation got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!"},fr:{"Chat with us!":"Chattez avec nous!","Scroll down to see new messages":"Faites défiler pour lire les nouveaux messages",Online:"En-ligne",Offline:"Hors-ligne",Connecting:"Connexion en cours","Connection re-established":"Connexion rétablie",Today:"Aujourdhui",Send:"Envoyer","Chat closed by %s":"Chat fermé par %s","Compose your message...":"Composez votre message...","All colleagues are busy.":"Tous les collègues sont actuellement occupés.","You are on waiting list position %s.":"Vous êtes actuellement en %s position dans la file d'attente.","Start new conversation":"Démarrer une nouvelle conversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation avec %s va être fermée.","Since you didn't respond in the last %s minutes your conversation got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Je vous remercie!"},nl:{"Chat with us!":"Chat met ons!","Scroll down to see new messages":"Scrol naar beneden om nieuwe berichten te zien",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbinding herstelt",Today:"Vandaag",Send:"Verzenden","Chat closed by %s":"Chat gesloten door %s","Compose your message...":"Typ uw bericht...","All colleagues are busy.":"Alle medewerkers zijn bezet.","You are on waiting list position %s.":"U bent %s in de wachtrij.","Start new conversation":"Nieuwe conversatie starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!"},it:{"Chat with us!":"Chatta con noi!","Scroll down to see new messages":"Scorrere verso il basso per vedere i nuovi messaggi",Online:"Online",Offline:"Offline",Connecting:"Collegamento","Connection re-established":"Collegamento ristabilito",Today:"Oggi",Send:"Invio","Chat closed by %s":"Conversazione chiusa da %s","Compose your message...":"Comporre il tuo messaggio...","All colleagues are busy.":"Tutti i colleghi sono occupati.","You are on waiting list position %s.":"Siete in posizione lista d' attesa %s.","Start new conversation":"Avviare una nuova conversazione","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione con %s si è chiusa.","Since you didn't respond in the last %s minutes your conversation got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione si è chiusa.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Ci dispiace, ci vuole più tempo come previsto per ottenere uno slot vuoto. Per favore riprova più tardi o inviaci un' e-mail. Grazie!"},pl:{"Chat with us!":"Czatuj z nami!","Scroll down to see new messages":"Przewiń w dół, aby wyświetlić nowe wiadomości",Online:"Online",Offline:"Offline",Connecting:"Łączenie","Connection re-established":"Ponowne nawiązanie połączenia",Today:"dzisiejszy",Send:"Wyślij","Chat closed by %s":"Czat zamknięty przez %s","Compose your message...":"Utwórz swoją wiadomość...","All colleagues are busy.":"Wszyscy koledzy są zajęci.","You are on waiting list position %s.":"Na liście oczekujących znajduje się pozycja %s.","Start new conversation":"Rozpoczęcie nowej konwersacji","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z %s została zamknięta.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!"},"zh-cn":{"Chat with us!":"发起即时对话!","Scroll down to see new messages":"向下滚动以查看新消息",Online:"在线",Offline:"离线",Connecting:"连接中","Connection re-established":"正在重新建立连接",Today:"今天",Send:"发送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在输入信息...","All colleagues are busy.":"所有工作人员都在忙碌中.","You are on waiting list position %s.":"您目前的等候位置是第 %s 位.","Start new conversation":"开始新的会话","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.","Since you didn't respond in the last %s minutes your conversation got closed.":"由于您超过 %s 分钟没有任何回复, 该对话已被关闭.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!"},"zh-tw":{"Chat with us!":"開始即時對话!","Scroll down to see new messages":"向下滑動以查看新訊息",Online:"線上",Offline:"离线",Connecting:"連線中","Connection re-established":"正在重新建立連線中",Today:"今天",Send:"發送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在輸入訊息...","All colleagues are busy.":"所有服務人員都在忙碌中.","You are on waiting list position %s.":"你目前的等候位置是第 %s 順位.","Start new conversation":"開始新的對話","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.","Since you didn't respond in the last %s minutes your conversation got closed.":"由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!"},ru:{"Chat with us!":"Напишите нам!","Scroll down to see new messages":"Прокрутите, чтобы увидеть новые сообщения",Online:"Онлайн",Offline:"Оффлайн",Connecting:"Подключение","Connection re-established":"Подключение восстановлено",Today:"Сегодня",Send:"Отправить","Chat closed by %s":"%s закрыл чат","Compose your message...":"Напишите сообщение...","All colleagues are busy.":"Все сотрудники заняты","You are on waiting list position %s.":"Вы в списке ожидания под номером %s","Start new conversation":"Начать новую переписку.","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.","Since you didn't respond in the last %s minutes your conversation got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!"}},n.prototype.sessionId=void 0,n.prototype.scrolledToBottom=!0,n.prototype.scrollSnapTolerance=10,n.prototype.richTextFormatKey={66:!0,73:!0,85:!0,83:!0},n.prototype.T=function(){var e,t,s,n,o,i;if(o=arguments[0],t=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?(i=this.translations[this.options.lang],i[o]||this.log.notice("Translation needed for '"+o+"'"),o=i[o]||o):this.log.notice("Translation '"+this.options.lang+"' needed!")),t)for(s=0,n=t.length;ss?t:document.body)},n.prototype.render=function(){return this.el&&document.querySelector(".zammad-chat")||this.renderBase(),document.querySelector("."+this.options.buttonClass).classList.add(this.inactiveClass),this.setAgentOnlineState("online"),this.log.debug("widget rendered"),this.startTimeoutObservers(),this.idleTimeout.start(),this.sessionId=sessionStorage.getItem("sessionId"),this.send("chat_status_customer",{session_id:this.sessionId,url:e.location.href})},n.prototype.renderBase=function(){return this.el&&this.el.remove(),this.options.target.innerHTML+=this.view("chat")({title:this.options.title,scrollHint:this.options.scrollHint}),this.el=this.options.target.querySelector(".zammad-chat"),this.input=this.el.querySelector(".zammad-chat-input"),this.body=this.el.querySelector(".zammad-chat-body"),this.el.querySelector(".js-chat-open").addEventListener("click",this.open),this.el.querySelector(".js-chat-toggle").addEventListener("click",this.toggle),this.el.querySelector(".js-chat-status").addEventListener("click",this.stopPropagation),this.el.querySelector(".zammad-chat-controls").addEventListener("submit",this.onSubmit),this.body.addEventListener("scroll",this.detectScrolledtoBottom),this.el.querySelector(".zammad-scroll-hint").addEventListener("click",this.onScrollHintClick),this.input.addEventListener("keydown",this.onKeydown),this.input.addEventListener("input",this.onInput),this.input.addEventListener("paste",this.onPaste),this.input.addEventListener("drop",this.onDrop),e.addEventListener("beforeunload",this.onLeaveTemporary),e.addEventListener("hashchange",function(t){return function(){return t.isOpen?void(t.sessionId&&t.send("chat_session_notice",{session_id:t.sessionId,message:e.location.href})):t.idleTimeout.start()}}(this))},n.prototype.stopPropagation=function(e){return e.stopPropagation()},n.prototype.onPaste=function(t){var s,n,o,i,a;if(t.stopPropagation(),t.preventDefault(),e.dataTransfer)s=e.dataTransfer;else{if(!t.dataTransfer)throw"No clipboardData support";s=t.dataTransfer}if(i=t.clientX,a=t.clientY,n=s.files[0],n.type.match("image.*"))return o=new FileReader,o.onload=function(e){return function(t){var s;return s=function(t,s){var n,o,r,l;return e.isRetina()&&(s/=2),l=t,n=new Image,n.style.width="100%",n.style.maxWidth=s(NaN),n.src=l,document.caretPositionFromPoint?(o=document.caretPositionFromPoint(i,a),r=document.createRange(),r.setStart(o.offsetNode,o.offset),r.collapse(),r.insertNode(n)):document.caretRangeFromPoint?(r=document.caretRangeFromPoint(i,a),r.insertNode(n)):console.log("could not find carat")},e.resizeImage(t.target.result,460,"auto",2,"image/jpeg","auto",s)}}(this),o.readAsDataURL(n)},n.prototype.onPaste=function(t){var s,n,o,i,a,r,l,c,d,h,u,p,m,g,f,y,v,b,w,T,S,C,z,L,k,I,A;if(t.stopPropagation(),t.preventDefault(),t.clipboardData)s=t.clipboardData;else if(e.clipboardData)s=e.clipboardData;else{if(!t.clipboardData)throw"No clipboardData support";s=t.clipboardData}if(r=!1,s&&s.items&&s.items[0]&&(l=s.items[0],"file"!==l.kind||"image/png"!==l.type&&"image/jpeg"!==l.type||(a=l.getAsFile(),T=new FileReader,T.onload=function(e){return function(t){var s;return s=function(t,s){var n;return e.isRetina()&&(s/=2),n=new Image,n.style.width="100%",n.style.maxWidth=s(NaN),n.src=t,document.execCommand("insertHTML",!1,n)},e.resizeImage(t.target.result,460,"auto",2,"image/jpeg","auto",s)}}(this),T.readAsDataURL(a),r=!0)),!r){A=void 0,n=void 0;try{A=s.getData("text/html"),n="html",A&&0!==A.length||(n="text",A=s.getData("text/plain")),A&&0!==A.length||(n="text2",A=s.getData("text"))}catch(_){t=_,console.log("Sorry, can't insert markup because browser is not supporting it."),n="text3",A=s.getData("text")}if("text"!==n&&"text2"!==n&&"text3"!==n||(A="
                  "+A.replace(/\n/g,"
                  ")+"
                  ",A=A.replace(/
                  <\/div>/g,"

                  ")),console.log("p",n,A),"html"===n){for(o=document.createElement("div"),o.innerHTML=A,y=!1,i=A,k=new RegExp("<(/w|w):[A-Za-z]"),i.match(k)&&(y=!0,i=i.replace(k,"")),k=new RegExp("<(/o|o):[A-Za-z]"),i.match(k)&&(y=!0,i=i.replace(k,"")),y&&(o=this.wordFilter(o)),S=o.childNodes,c=0,u=S.length;cnew Date((new Date).getTime()-1500)))return this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start()},n.prototype.onSubmit=function(e){return e.preventDefault(),this.sendMessage()},n.prototype.sendMessage=function(){var e,t;if(e=this.input.innerHTML)return this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),t=this.view("message")({message:e,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.querySelector(".zammad-chat-message--typing")?(this.lastAddedType="typing-placeholder",this.el.querySelector(".zammad-chat-message--typing").insertAdjacentHTML("beforebegin",t)):(this.lastAddedType="message--customer",this.body.insertAdjacentHTML("beforeend",t)),this.input.innerHTML="",this.scrollToBottom(),this.send("chat_session_message",{content:e,id:this._messageCount,session_id:this.sessionId})},n.prototype.receiveMessage=function(e){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:e.message.content,id:e.id,from:"agent"}),this.scrollToBottom({showHint:!0})},n.prototype.renderMessage=function(e){return this.lastAddedType="message--"+e.from,e.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.body.insertAdjacentHTML("beforeend",this.view("message")(e))},n.prototype.open=function(){var t;return this.isOpen?void this.log.debug("widget already open, block"):(this.isOpen=!0,this.log.debug("open widget"),this.show(),this.sessionId||this.showLoader(),this.el.classList.add("zammad-chat-is-open"),t=this.el.clientHeight-this.el.querySelector(".zammad-chat-header").offsetHeight,this.el.style.transform="translateY("+t+"px)",this.el.clientHeight,this.sessionId?(this.el.style.transform="",this.onOpenTransitionend()):(this.el.addEventListener("transitionend",this.onOpenTransitionend),this.el.classList.add("zammad-chat--animate"),this.el.clientHeight,this.el.style.transform="",this.send("chat_session_init",{url:e.location.href})))},n.prototype.onOpenTransitionend=function(){if(this.el.removeEventListener("transitionend",this.onOpenTransitionend),this.el.classList.remove("zammad-chat--animate"),this.idleTimeout.stop(),this.isFullscreen)return this.disableScrollOnRoot()},n.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},n.prototype.toggle=function(e){return this.isOpen?this.close(e):this.open(e)},n.prototype.close=function(e){var t;return this.isOpen?(this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId?(this.log.debug("close widget"),e&&e.stopPropagation(),this.sessionClose(),this.isFullscreen&&this.enableScrollOnRoot(),t=this.el.clientHeight-this.el.querySelector(".zammad-chat-header").offsetHeight,this.el.addEventListener("transitionend",this.onCloseTransitionend), +this.el.classList.add("zammad-chat--animate"),document.offsetHeight,this.el.style.transform="translateY("+t+"px)"):void this.log.debug("can't close widget without sessionId")):void this.log.debug("can't close widget, it's not open")},n.prototype.onCloseTransitionend=function(){return this.el.removeEventListener("transitionend",this.onCloseTransitionend),this.el.classList.remove("zammad-chat-is-open","zammad-chat--animate"),this.el.style.transform="",this.showLoader(),this.el.querySelector(".zammad-chat-welcome").classList.remove("zammad-chat-is-hidden"),this.el.querySelector(".zammad-chat-agent").classList.add("zammad-chat-is-hidden"),this.el.querySelector(".zammad-chat-agent-status").classList.add("zammad-chat-is-hidden"),this.isOpen=!1,this.io.reconnect()},n.prototype.onWebSocketClose=function(){if(!this.isOpen)return this.el?(this.el.classList.remove("zammad-chat-is-shown"),this.el.classList.remove("zammad-chat-is-loaded")):void 0},n.prototype.show=function(){if("offline"!==this.state)return this.el.classList.add("zammad-chat-is-loaded"),this.el.classList.add("zammad-chat-is-shown")},n.prototype.disableInput=function(){return this.input.disabled=!0,this.el.querySelector(".zammad-chat-send").disabled=!0},n.prototype.enableInput=function(){return this.input.disabled=!1,this.el.querySelector(".zammad-chat-send").disabled=!1},n.prototype.hideModal=function(){return this.el.querySelector(".zammad-chat-modal").innerHTML=""},n.prototype.onQueueScreen=function(e){var t;return this.setSessionId(e.session_id),t=function(t){return function(){return t.onQueue(e),t.waitingListTimeout.start()}}(this),this.initialQueueDelay&&!this.onInitialQueueDelayId?void(this.onInitialQueueDelayId=setTimeout(t,this.initialQueueDelay)):(this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),t())},n.prototype.onQueue=function(e){return this.log.notice("onQueue",e.position),this.inQueue=!0,this.el.querySelector(".zammad-chat-modal").innerHTML=this.view("waiting")({position:e.position})},n.prototype.onAgentTypingStart=function(){if(this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.querySelector(".zammad-chat-message--typing")&&(this.maybeAddTimestamp(),this.body.insertAdjacentHTML("beforeend",this.view("typingIndicator")()),this.isVisible(this.el.querySelector(".zammad-chat-message--typing"),!0)))return this.scrollToBottom()},n.prototype.onAgentTypingEnd=function(){if(this.el.querySelector(".zammad-chat-message--typing"))return this.el.querySelector(".zammad-chat-message--typing").remove()},n.prototype.onLeaveTemporary=function(){if(this.sessionId)return this.send("chat_session_leave_temporary",{session_id:this.sessionId})},n.prototype.maybeAddTimestamp=function(){var e,t,s;if(s=Date.now(),!this.lastTimestamp||s-this.lastTimestamp>6e4*this.showTimeEveryXMinutes)return e=this.T("Today"),t=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(e,t),this.lastTimestamp=s):(this.body.insertAdjacentHTML("beforeend",this.view("timestamp")({label:e,time:t})),this.lastTimestamp=s,this.lastAddedType="timestamp",this.scrollToBottom())},n.prototype.updateLastTimestamp=function(e,t){var s;if(this.el&&(s=this.el.querySelectorAll(".zammad-chat-body .zammad-chat-timestamp")))return s[s.length-1].outerHTML=this.view("timestamp")({label:e,time:t})},n.prototype.addStatus=function(e){if(this.el)return this.maybeAddTimestamp(),this.body.insertAdjacentHTML("beforeend",this.view("status")({status:e})),this.scrollToBottom()},n.prototype.detectScrolledtoBottom=function(){var e;if(e=this.body.scrollTop+this.body.offsetHeight,this.scrolledToBottom=Math.abs(e-this.body.scrollHeight)<=this.scrollSnapTolerance,this.scrolledToBottom)return this.el.querySelector(".zammad-scroll-hint").classList.add("is-hidden")},n.prototype.showScrollHint=function(){return this.el.querySelector(".zammad-scroll-hint").classList.remove("is-hidden"),this.body.scrollTop=this.body.scrollTop+this.el.querySelector(".zammad-scroll-hint").offsetHeight},n.prototype.onScrollHintClick=function(){return this.body.scrollTo({top:this.body.scrollHeight,behavior:"smooth"})},n.prototype.scrollToBottom=function(e){var t;return t=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.body.scrollTop=this.body.scrollHeight:t?this.showScrollHint():void 0},n.prototype.destroy=function(e){return null==e&&(e={}),this.log.debug("destroy widget",e),this.setAgentOnlineState("offline"),e.remove&&this.el&&this.el.remove(),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},n.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},n.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established"))},n.prototype.onSessionClosed=function(e){return this.addStatus(this.T("Chat closed by %s",e.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop()},n.prototype.setSessionId=function(e){return this.sessionId=e,void 0===e?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",e)},n.prototype.onConnectionEstablished=function(e){return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,e.agent&&(this.agent=e.agent),e.session_id&&this.setSessionId(e.session_id),this.body.innerHTML="",this.el.querySelector(".zammad-chat-agent").innerHTML=this.view("agent")({agent:this.agent}),this.enableInput(),this.hideModal(),this.el.querySelector(".zammad-chat-welcome").classList.add("zammad-chat-is-hidden"),this.el.querySelector(".zammad-chat-agent").classList.remove("zammad-chat-is-hidden"),this.el.querySelector(".zammad-chat-agent-status").classList.remove("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start()},n.prototype.showCustomerTimeout=function(){return this.el.querySelector(".zammad-chat-modal").innerHTML=this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout}),this.el.querySelector(".js-restart").addEventListener("click",function(){return location.reload()}),this.sessionClose()},n.prototype.showWaitingListTimeout=function(){return this.el.querySelector(".zammad-chat-modal").innerHTML=this.view("waiting_list_timeout")({delay:this.options.watingListTimeout}),this.el.querySelector(".js-restart").addEventListener("click",function(){return location.reload()}),this.sessionClose()},n.prototype.showLoader=function(){return this.el.querySelector(".zammad-chat-modal").innerHTML=this.view("loader")()},n.prototype.setAgentOnlineState=function(e){var t;if(this.state=e,this.el)return t=e.charAt(0).toUpperCase()+e.slice(1),this.el.querySelector(".zammad-chat-agent-status").dataset.status=e,this.el.querySelector(".zammad-chat-agent-status").textContent=this.T(t)},n.prototype.detectHost=function(){var e;return e="ws://","https"===d&&(e="wss://"),this.options.host=""+e+c+"/ws"},n.prototype.loadCss=function(){var e,t,s;if(this.options.cssAutoload)return s=this.options.cssUrl,s||(s=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws/i,""),s+="/assets/chat/chat.css"),this.log.debug("load css from '"+s+"'"),t="@import url('"+s+"');",e=document.createElement("link"),e.onload=this.onCssLoaded,e.rel="stylesheet",e.href="data:text/css,"+escape(t),document.getElementsByTagName("head")[0].appendChild(e)},n.prototype.onCssLoaded=function(){if(this.cssLoaded=!0,this.socketReady)return this.onReady()},n.prototype.startTimeoutObservers=function(){return this.idleTimeout=new a({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:function(e){return function(){return e.log.debug("Idle timeout reached, hide widget",new Date),e.destroy({remove:!0})}}(this)}),this.inactiveTimeout=new a({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:function(e){return function(){return e.log.debug("Inactive timeout reached, show timeout screen.",new Date),e.showCustomerTimeout(),e.destroy({remove:!1})}}(this)}),this.waitingListTimeout=new a({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:function(e){return function(){return e.log.debug("Waiting list timeout reached, show timeout screen.",new Date),e.showWaitingListTimeout(),e.destroy({remove:!1})}}(this)})},n.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop,this.scrollRoot.style.overflow="hidden",this.scrollRoot.style.position="fixed"},n.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop=this.rootScrollOffset(NaN),this.scrollRoot.style.overflow="",this.scrollRoot.style.position=""},n.prototype.isVisible=function(s,n,o,i){var a,r,l,c,d,h,u,p,m,g;if(!(s.length<1))return g=e.innerWidth,m=e.innerHeight,i=i?i:"both",r=o!==!0||t.offsetWidth*t.offsetHeight,h=s.getBoundingClientRect(),u=h.top>=0&&h.top0&&h.bottom<=m,c=h.left>=0&&h.left0&&h.right<=g,p=n?u||a:u&&a,l=n?c||d:c&&d,"both"===i?r&&p&&l:"vertical"===i?r&&p:"horizontal"===i?r&&l:void 0},n.prototype.isRetina=function(){var t;return!!e.matchMedia&&(t=e.matchMedia("only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)"),t&&t.matches||e.devicePixelRatio>1)},n.prototype.resizeImage=function(e,t,s,n,o,i,a,r){var l;return null==t&&(t="auto"),null==s&&(s="auto"),null==n&&(n=1),null==r&&(r=!0),l=new Image,l.onload=function(){var e,r,c,d,h,u,p;return h=l.width,d=l.height,console.log("ImageService","current size",h,d),"auto"===s&&"auto"===t&&(t=h,s=d),"auto"===s&&(c=h/t,s=d/c),"auto"===t&&(c=h/s,t=d/c),p=!1,t/gi,""),t=t.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,""),t=t.replace(/<(\/?)s>/gi,"<$1strike>"),t=t.replace(/ /gi," "),e.innerHTML=t,k=e.querySelectorAll("p"),i=0,c=k.length;i",/^\s*\w+\./.test(W)&&(b=/([0-9])\./.exec(W),b?(P=parseInt(b[1],10),y=null!=(_=P>1)?_:'
                    ':"
                      "}):y="
                        "),s>l&&(0===l&&(S.insertAdjacentHTML("beforebegin",y),C=S.previousElementSibling),C.insertAdjacentHTML("beforeend",y)),s=q;o=x<=q?++r:--r)C=C.parentNode;S.querySelector("span:first")&&S.querySelector("span:first").remove(),C.insertAdjacentHTML("beforeend","
                      1. "+S.innerHTML+"
                      2. "),S.remove(),l=s}else l=0;for(H=e.querySelectorAll("[style]"),v=0,h=H.length;v/g,">").replace(/"/g,""")}),function(){(function(){s.push('
                        \n
                        \n
                        \n \n \n \n \n \n
                        \n
                        \n
                        \n
                        \n \n '),s.push(this.T(this.title)),s.push('\n
                        \n
                        \n
                        \n \n
                        \n
                        \n
                        \n \n
                        \n
                        ")}).call(this)}.call(e),e.safe=o,e.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(e){e||(e={});var t,s=[],n=function(e){return e&&e.ecoSafe?e:"undefined"!=typeof e&&null!=e?i(e):""},o=e.safe,i=e.escape;return t=e.safe=function(e){if(e&&e.ecoSafe)return e;"undefined"!=typeof e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},i||(i=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                        \n '),this.agent?(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent)),s.push("\n ")):(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay)),s.push("\n ")),s.push('\n
                        \n
                        "),s.push(this.T("Start new conversation")),s.push("
                        \n
                        ")}).call(this)}.call(e),e.safe=o,e.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(e){e||(e={});var t,s=[],n=e.safe,o=e.escape;return t=e.safe=function(e){if(e&&e.ecoSafe)return e;"undefined"!=typeof e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('\n \n \n \n\n'),s.push(this.T("Connecting")),s.push("")}).call(this)}.call(e),e.safe=n,e.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(e){e||(e={});var t,s=[],n=function(e){return e&&e.ecoSafe?e:"undefined"!=typeof e&&null!=e?i(e):""},o=e.safe,i=e.escape;return t=e.safe=function(e){if(e&&e.ecoSafe)return e;"undefined"!=typeof e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},i||(i=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                        \n "),s.push(this.message),s.push("\n
                        ")}).call(this)}.call(e),e.safe=o,e.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(e){e||(e={});var t,s=[],n=e.safe,o=e.escape;return t=e.safe=function(e){if(e&&e.ecoSafe)return e;"undefined"!=typeof e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                        \n
                        \n '),s.push(this.status),s.push("\n
                        \n
                        ")}).call(this)}.call(e),e.safe=n,e.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(e){e||(e={});var t,s=[],n=function(e){return e&&e.ecoSafe?e:"undefined"!=typeof e&&null!=e?i(e):""},o=e.safe,i=e.escape;return t=e.safe=function(e){if(e&&e.ecoSafe)return e;"undefined"!=typeof e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},i||(i=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                        '),s.push(n(this.label)),s.push(" "),s.push(n(this.time)),s.push("
                        ")}).call(this)}.call(e),e.safe=o,e.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(e){e||(e={});var t,s=[],n=e.safe,o=e.escape;return t=e.safe=function(e){if(e&&e.ecoSafe)return e;"undefined"!=typeof e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                        \n \n \n \n \n \n \n \n
                        ')}).call(this)}.call(e),e.safe=n,e.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(e){e||(e={});var t,s=[],n=e.safe,o=e.escape;return t=e.safe=function(e){if(e&&e.ecoSafe)return e;"undefined"!=typeof e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                        \n \n \n \n \n \n '),s.push(this.T("All colleagues are busy.")),s.push("
                        \n "),s.push(this.T("You are on waiting list position %s.",this.position)),s.push("\n
                        ")}).call(this)}.call(e),e.safe=n,e.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(e){e||(e={});var t,s=[],n=function(e){return e&&e.ecoSafe?e:"undefined"!=typeof e&&null!=e?i(e):""},o=e.safe,i=e.escape;return t=e.safe=function(e){if(e&&e.ecoSafe)return e;"undefined"!=typeof e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},i||(i=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
                        \n '),s.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),s.push('\n
                        \n
                        "),s.push(this.T("Start new conversation")),s.push("
                        \n
                        ")}).call(this)}.call(e),e.safe=o,e.escape=i,s.join("")}; \ No newline at end of file diff --git a/public/assets/chat/chat.css b/public/assets/chat/chat.css index 81d219049..b9b99c25a 100644 --- a/public/assets/chat/chat.css +++ b/public/assets/chat/chat.css @@ -10,7 +10,6 @@ border-radius: 5px 5px 0 0; will-change: bottom; display: none; - -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; z-index: 999; } @@ -20,9 +19,10 @@ width: 100%; border-radius: 0 !important; font-size: 16px; } } + .zammad-chat--animate { + transition: transform 500ms; } .zammad-chat.zammad-chat-is-loaded { - display: -webkit-flex; display: -ms-flexbox; display: flex; opacity: 0; } @@ -49,6 +49,7 @@ background: #379ad7; color: white; line-height: 2.5em; + height: 2.5em; box-shadow: 0 -1px rgba(0, 0, 0, 0.1), 0 1px rgba(255, 255, 255, 0.3) inset, 0 -1px rgba(0, 0, 0, 0.1) inset, 0 1px 1px rgba(0, 0, 0, 0.13); position: relative; border-radius: 5px 5px 0 0; @@ -134,28 +135,15 @@ background: #52c782; } .zammad-chat-agent-status[data-status="connecting"]:before { - -webkit-animation: linear connect-fade 600ms infinite alternate; animation: linear connect-fade 600ms infinite alternate; background: #faab00; } -@-webkit-keyframes connect-fade { - from { - opacity: .5; - -webkit-transform: scale(0.6); - transform: scale(0.6); } - to { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1); } } - @keyframes connect-fade { from { opacity: .5; - -webkit-transform: scale(0.6); transform: scale(0.6); } to { opacity: 1; - -webkit-transform: scale(1); transform: scale(1); } } .zammad-chat-modal { @@ -169,13 +157,10 @@ background: white; z-index: 1; padding: 20px; - display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-align-items: center; -ms-flex-align: center; align-items: center; - -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; } .zammad-chat-modal:empty { @@ -196,10 +181,8 @@ .zammad-scroll-hint { background: #f9fafa; - display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-align-items: center; -ms-flex-align: center; align-items: center; border-bottom: 1px solid #e8e8e8; @@ -217,15 +200,14 @@ padding: 0.5em 1em; overflow: auto; background: white; - -webkit-flex: 1; -ms-flex: 1; flex: 1; display: none; - -webkit-overflow-scrolling: touch; } + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; } @media only screen and (max-width: 768px) { .zammad-chat-body { height: auto; - -webkit-flex: 1; -ms-flex: 1; flex: 1; } } @@ -289,41 +271,25 @@ height: 0.55em; width: 0.55em; display: inline-block; - -webkit-animation: ease-in-out load-fade 600ms infinite alternate; animation: ease-in-out load-fade 600ms infinite alternate; } .zammad-chat-loading-circle + .zammad-chat-loading-circle { - -webkit-animation-delay: .13s; animation-delay: .13s; } .zammad-chat-loading-circle + .zammad-chat-loading-circle + .zammad-chat-loading-circle { - -webkit-animation-delay: .26s; animation-delay: .26s; } -@-webkit-keyframes load-fade { - from { - opacity: .5; - -webkit-transform: scale(0.6); - transform: scale(0.6); } - 67% { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1); } } - @keyframes load-fade { from { opacity: .5; - -webkit-transform: scale(0.6); transform: scale(0.6); } 67% { opacity: 1; - -webkit-transform: scale(1); transform: scale(1); } } .zammad-chat-controls { overflow: hidden; display: none; - -webkit-align-items: flex-end; -ms-flex-align: end; align-items: flex-end; border-top: 1px solid #ededed; @@ -335,7 +301,6 @@ background: white; } .zammad-chat-is-open .zammad-chat-controls { - display: -webkit-flex; display: -ms-flexbox; display: flex; } @@ -356,7 +321,6 @@ box-shadow: none; box-sizing: content-box; outline: none; - -webkit-flex: 1; -ms-flex: 1; flex: 1; overflow: auto; } diff --git a/public/assets/chat/chat.scss b/public/assets/chat/chat.scss index 2c85d78fc..4932d95a3 100644 --- a/public/assets/chat/chat.scss +++ b/public/assets/chat/chat.scss @@ -19,6 +19,10 @@ border-radius: 0 !important; font-size: 16px; } + + &--animate { + transition: transform 500ms; + } } .zammad-chat.zammad-chat-is-loaded { display: flex; @@ -50,6 +54,7 @@ background: hsl(203,67%,53%); color: white; line-height: 2.5em; + height: 2.5em; box-shadow: 0 -1px rgba(0,0,0,.1), 0 1px rgba(255,255,255,.3) inset, @@ -231,6 +236,7 @@ flex: 1; display: none; -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; @media only screen and (max-width: 768px) { height: auto; diff --git a/public/assets/chat/gulpfile.js b/public/assets/chat/gulpfile.js index c2be20ca3..b7fcb0fee 100644 --- a/public/assets/chat/gulpfile.js +++ b/public/assets/chat/gulpfile.js @@ -37,6 +37,23 @@ gulp.task('js', function(){ .pipe(gulp.dest('./')); }); + +gulp.task('no-jquery', function(){ + var templates = gulp.src('views/*.eco') + .pipe(eco({namespace: 'zammadChatTemplates'})); + + var js = gulp.src('chat-no-jquery.coffee') + .pipe(plumber()) + .pipe(coffee({bare: true}).on('error', gutil.log)); + + return merge(templates, js) + .pipe(concat('chat-no-jquery.js')) + .pipe(gulp.dest('./')) + .pipe(uglify()) + .pipe(rename({ extname: '.min.js' })) + .pipe(gulp.dest('./')); +}); + gulp.task('default', function(){ var cssWatcher = gulp.watch(['chat.scss'], ['css']); cssWatcher.on('change', function(event) { @@ -47,4 +64,9 @@ gulp.task('default', function(){ jsWatcher.on('change', function(event) { console.log('File ' + event.path + ' was ' + event.type + ', running tasks...'); }); + + var js2Watcher = gulp.watch(['chat-no-jquery.coffee'], ['no-jquery']); + js2Watcher.on('change', function(event) { + console.log('File ' + event.path + ' was ' + event.type + ', running tasks...'); + }); }); diff --git a/public/assets/chat/znuny-no-jquery-open_by_button.html b/public/assets/chat/znuny-no-jquery-open_by_button.html new file mode 100644 index 000000000..f43a0535a --- /dev/null +++ b/public/assets/chat/znuny-no-jquery-open_by_button.html @@ -0,0 +1,174 @@ + + + + + Zammad Chat + + + + + + + + + + + + + + + + + + +
                        + + + + + + + + +

                        Settings

                        +
                        +
                        + + + +
                        + + + +
                        + + 12px + + +
                        + +

                        Log

                        +
                        +
                        +
                        +
                        + + + + + \ No newline at end of file diff --git a/public/assets/chat/znuny-no-jquery.html b/public/assets/chat/znuny-no-jquery.html new file mode 100644 index 000000000..ea1d5ba8b --- /dev/null +++ b/public/assets/chat/znuny-no-jquery.html @@ -0,0 +1,173 @@ + + + + + Zammad Chat + + + + + + + + + + + + + + + + + + +
                        + + + + + + + + +

                        Settings

                        +
                        +
                        + + + +
                        + + + +
                        + + 12px + + +
                        + +

                        Log

                        +
                        +
                        +
                        +
                        + + + + + \ No newline at end of file diff --git a/public/assets/chat/znuny.html b/public/assets/chat/znuny.html index 51b69fb15..db38a0165 100644 --- a/public/assets/chat/znuny.html +++ b/public/assets/chat/znuny.html @@ -157,7 +157,6 @@ debug: true, background: '#494d52', flat: true, - show: true, idleTimeout: 1, idleTimeoutIntervallCheck: 0.5, inactiveTimeout: 2, diff --git a/script/build/test_slice_tests.sh b/script/build/test_slice_tests.sh index 3a1105492..25577293e 100755 --- a/script/build/test_slice_tests.sh +++ b/script/build/test_slice_tests.sh @@ -59,6 +59,7 @@ if [ "$LEVEL" == '1' ]; then rm test/browser/agent_user_profile_test.rb # test/browser/auth_test.rb rm test/browser/chat_test.rb + rm test/browser/chat_no_jquery_test.rb rm test/browser/customer_ticket_create_fields_test.rb rm test/browser/customer_ticket_create_test.rb rm test/browser/first_steps_test.rb @@ -140,6 +141,7 @@ elif [ "$LEVEL" == '2' ]; then rm test/browser/agent_user_profile_test.rb rm test/browser/auth_test.rb rm test/browser/chat_test.rb + rm test/browser/chat_no_jquery_test.rb rm test/browser/customer_ticket_create_fields_test.rb rm test/browser/customer_ticket_create_test.rb rm test/browser/first_steps_test.rb @@ -221,6 +223,7 @@ elif [ "$LEVEL" == '3' ]; then rm test/browser/agent_user_profile_test.rb rm test/browser/auth_test.rb rm test/browser/chat_test.rb + rm test/browser/chat_no_jquery_test.rb rm test/browser/customer_ticket_create_fields_test.rb rm test/browser/customer_ticket_create_test.rb rm test/browser/first_steps_test.rb @@ -302,6 +305,7 @@ elif [ "$LEVEL" == '4' ]; then rm test/browser/agent_user_profile_test.rb rm test/browser/auth_test.rb rm test/browser/chat_test.rb + rm test/browser/chat_no_jquery_test.rb # test/browser/customer_ticket_create_fields_test.rb # test/browser/customer_ticket_create_test.rb rm test/browser/first_steps_test.rb @@ -382,6 +386,7 @@ elif [ "$LEVEL" == '5' ]; then # test/browser/agent_user_profile_test.rb rm test/browser/auth_test.rb rm test/browser/chat_test.rb + rm test/browser/chat_no_jquery_test.rb rm test/browser/customer_ticket_create_fields_test.rb rm test/browser/customer_ticket_create_test.rb rm test/browser/first_steps_test.rb @@ -465,6 +470,7 @@ elif [ "$LEVEL" == '6' ]; then rm test/browser/agent_user_profile_test.rb rm test/browser/auth_test.rb # test/browser/chat_test.rb + # test/browser/chat_no_jquery_test.rb rm test/browser/customer_ticket_create_fields_test.rb rm test/browser/customer_ticket_create_test.rb # test/browser/first_steps_test.rb diff --git a/test/browser/chat_no_jquery_test.rb b/test/browser/chat_no_jquery_test.rb new file mode 100644 index 000000000..4a225d72d --- /dev/null +++ b/test/browser/chat_no_jquery_test.rb @@ -0,0 +1,864 @@ +require 'browser_test_helper' + +class ChatNoJqueryTest < TestCase + + def test_basic + chat_url = "#{browser_url}/assets/chat/znuny-no-jquery.html?port=#{ENV['WS_PORT']}" + agent = browser_instance + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + + # disable chat + click( + browser: agent, + css: 'a[href="#manage"]', + ) + click( + browser: agent, + css: '.content.active a[href="#channels/chat"]', + ) + switch( + browser: agent, + css: '.content.active .js-chatSetting', + type: 'off', + ) + + # nav bar shuld be gone + sleep 2 + exists_not( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + sleep 15 + + customer = browser_instance + location( + browser: customer, + url: chat_url, + ) + sleep 4 + exists_not( + browser: customer, + css: '.zammad-chat', + ) + match( + browser: customer, + css: '.settings', + value: '{"state":"chat_disabled"}', + ) + click( + browser: agent, + css: 'a[href="#manage"]', + ) + click( + browser: agent, + css: '.content.active a[href="#channels/chat"]', + ) + switch( + browser: agent, + css: '.content.active .js-chatSetting', + type: 'on', + ) + sleep 15 # wait for rerendering + switch( + browser: agent, + css: '#navigation .js-chatMenuItem .js-switch', + type: 'off', + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + wait: 2, + ) + match_not( + browser: agent, + css: '.active.content', + value: 'disabled', + ) + + reload( + browser: customer, + ) + sleep 4 + exists_not( + browser: customer, + css: '.zammad-chat', + ) + match_not( + browser: customer, + css: '.settings', + value: '{"state":"chat_disabled"}', + ) + match( + browser: customer, + css: '.settings', + value: '{"event":"chat_status_customer","data":{"state":"offline"}}', + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + switch( + browser: agent, + css: '#navigation .js-chatMenuItem .js-switch', + type: 'on', + ) + reload( + browser: customer, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + timeout: 5, + ) + match_not( + browser: customer, + css: '.settings', + value: '{"state":"chat_disabled"}', + ) + match_not( + browser: customer, + css: '.settings', + value: '{"event":"chat_status_customer","data":{"state":"offline"}}', + ) + match( + browser: customer, + css: '.settings', + value: '"data":{"state":"online"}', + ) + + # init chat + click( + browser: customer, + css: '.zammad-chat .js-chat-open', + ) + exists( + browser: customer, + css: '.zammad-chat-is-shown', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + watch_for( + browser: agent, + css: '.js-chatMenuItem .counter', + value: '1', + ) + click( + browser: customer, + css: '.zammad-chat .js-chat-toggle .zammad-chat-header-icon', + ) + watch_for_disappear( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + watch_for_disappear( + browser: agent, + css: '.js-chatMenuItem .counter', + ) + + end + + def test_basic_usecase1 + chat_url = "#{browser_url}/assets/chat/znuny-no-jquery.html?port=#{ENV['WS_PORT']}" + agent = browser_instance + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + agent.find_elements(css: '.active .chat-window .js-disconnect:not(.is-hidden)').each(&:click) + agent.find_elements(css: '.active .chat-window .js-close').each(&:click) + + customer = browser_instance + location( + browser: customer, + url: chat_url, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + timeout: 5, + ) + click( + browser: customer, + css: '.js-chat-open', + ) + exists( + browser: customer, + css: '.zammad-chat-is-shown', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + + click( + browser: agent, + css: '.active .js-acceptChat', + ) + sleep 2 + exists_not( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + match( + browser: agent, + css: '.active .chat-window .js-body', + value: chat_url, + ) + set( + browser: agent, + css: '.active .chat-window .js-customerChatInput', + value: 'my name is me', + ) + click( + browser: agent, + css: '.active .chat-window .js-send', + ) + watch_for( + browser: customer, + css: '.zammad-chat .zammad-chat-agent-status', + value: 'online', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'my name is me', + ) + set( + browser: customer, + css: '.zammad-chat .zammad-chat-input', + value: 'my name is customer', + ) + click( + browser: customer, + css: '.zammad-chat .zammad-chat-send', + ) + watch_for( + browser: agent, + css: '.active .chat-window', + value: 'my name is customer', + ) + exists( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + click( + browser: agent, + css: '.active .chat-window .js-customerChatInput', + ) + exists_not( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + click( + browser: customer, + css: '.js-chat-toggle .zammad-chat-header-icon', + ) + watch_for( + browser: agent, + css: '.active .chat-window', + value: 'closed the conversation', + ) + end + + def test_basic_usecase2 + chat_url = "#{browser_url}/assets/chat/znuny-no-jquery.html?port=#{ENV['WS_PORT']}" + agent = browser_instance + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + agent.find_elements(css: '.active .chat-window .js-disconnect:not(.is-hidden)').each(&:click) + agent.find_elements(css: '.active .chat-window .js-close').each(&:click) + + customer = browser_instance + location( + browser: customer, + url: chat_url, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + timeout: 5, + ) + click( + browser: customer, + css: '.js-chat-open', + ) + exists( + browser: customer, + css: '.zammad-chat-is-shown', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + click( + browser: agent, + css: '.active .js-acceptChat', + ) + sleep 2 + exists_not( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + + # keep focus outside of chat window to check .chat-status.is-modified later + click( + browser: agent, + css: '#global-search', + ) + watch_for( + browser: customer, + css: '.zammad-chat .zammad-chat-agent-status', + value: 'online', + ) + set( + browser: customer, + css: '.zammad-chat .zammad-chat-input', + value: 'my name is customer', + ) + click( + browser: customer, + css: '.zammad-chat .zammad-chat-send', + ) + watch_for( + browser: agent, + css: '.active .chat-window', + value: 'my name is customer', + ) + exists( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + set( + browser: agent, + css: '.active .chat-window .js-customerChatInput', + value: 'my name is me', + ) + exists_not( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + click( + browser: agent, + css: '.active .chat-window .js-send', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'my name is me', + ) + click( + browser: agent, + css: '.active .chat-window .js-disconnect:not(.is-hidden)', + ) + click( + browser: agent, + css: '.active .chat-window .js-close', + ) + watch_for( + browser: customer, + css: '.zammad-chat .zammad-chat-agent-status', + value: 'offline', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(Chat closed by|Chat beendet von)', + ) + click( + browser: customer, + css: '.zammad-chat .js-chat-toggle .zammad-chat-header-icon', + ) + watch_for_disappear( + browser: customer, + css: '.zammad-chat-is-open', + ) + agent.find_elements(css: '.active .chat-window .js-disconnect:not(.is-hidden)').each(&:click) + agent.find_elements(css: '.active .chat-window .js-close').each(&:click) + sleep 2 + click( + browser: customer, + css: '.zammad-chat .js-chat-open', + ) + exists( + browser: customer, + css: '.zammad-chat-is-shown', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + click( + browser: agent, + css: '.active .js-acceptChat', + ) + sleep 2 + exists_not( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + exists( + browser: agent, + css: '.active .chat-window .chat-status', + ) + end + + def test_basic_usecase3 + chat_url = "#{browser_url}/assets/chat/znuny-no-jquery.html?port=#{ENV['WS_PORT']}" + agent = browser_instance + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + agent.find_elements(css: '.active .chat-window .js-disconnect:not(.is-hidden)').each(&:click) + agent.find_elements(css: '.active .chat-window .js-close').each(&:click) + + # set chat preferences + click( + browser: agent, + css: '.active .js-settings', + ) + + modal_ready(browser: agent) + set( + browser: agent, + css: '.modal [name="chat::phrase::1"]', + value: 'Hi Stranger!;My Greeting', + ) + click( + browser: agent, + css: '.modal .js-submit', + ) + modal_disappear(browser: agent) + + customer = browser_instance + location( + browser: customer, + url: chat_url, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + timeout: 5, + ) + click( + browser: customer, + css: '.js-chat-open', + ) + exists( + browser: customer, + css: '.zammad-chat-is-shown', + ) + watch_for( + browser: agent, + css: '.active .js-badgeWaitingCustomers', + value: '1', + ) + click( + browser: agent, + css: '.active .js-acceptChat', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'Hi Stranger|My Greeting', + ) + watch_for( + browser: customer, + css: '.zammad-chat .zammad-chat-agent-status', + value: 'online', + ) + match( + browser: agent, + css: '.active .chat-window .js-body', + value: chat_url, + ) + set( + browser: agent, + css: '.active .chat-window .js-customerChatInput', + value: 'my name is me', + ) + click( + browser: agent, + css: '.active .chat-window .js-send', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'my name is me', + ) + set( + browser: customer, + css: '.zammad-chat .zammad-chat-input', + value: 'my name is customer', + ) + click( + browser: customer, + css: '.zammad-chat .zammad-chat-send', + ) + watch_for( + browser: agent, + css: '.active .chat-window', + value: 'my name is customer', + ) + click( + browser: agent, + css: '.active .chat-window .js-customerChatInput', + ) + reload( + browser: customer, + ) + exists( + browser: customer, + css: '.zammad-chat', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'Hi Stranger|My Greeting', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'my name is me', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'my name is customer', + ) + location( + browser: customer, + url: "#{chat_url}#new_hash", + ) + sleep 2 + match( + browser: agent, + css: '.active .chat-window .js-body', + value: "#{chat_url}#new_hash", + ) + click( + browser: customer, + css: '.zammad-chat .js-chat-toggle .zammad-chat-header-icon', + ) + watch_for( + browser: agent, + css: '.active .chat-window', + value: 'closed the conversation', + ) + end + + def test_open_chat_by_button + chat_url = "#{browser_url}/assets/chat/znuny-no-jquery-open_by_button.html?port=#{ENV['WS_PORT']}" + agent = browser_instance + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + agent.find_elements(css: '.active .chat-window .js-disconnect:not(.is-hidden)').each(&:click) + agent.find_elements(css: '.active .chat-window .js-close').each(&:click) + + customer = browser_instance + location( + browser: customer, + url: chat_url, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + timeout: 5, + ) + exists_not( + browser: customer, + css: '.zammad-chat-is-shown', + ) + exists_not( + browser: customer, + css: '.zammad-chat-is-open', + ) + click( + browser: customer, + css: '.open-zammad-chat', + ) + watch_for( + browser: customer, + css: '.zammad-chat-is-shown', + timeout: 4, + ) + watch_for( + browser: customer, + css: '.zammad-chat-is-open', + timeout: 4, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + click( + browser: customer, + css: '.zammad-chat-header-icon-close', + ) + watch_for_disappear( + browser: customer, + css: '.zammad-chat-is-shown', + timeout: 4, + ) + watch_for_disappear( + browser: customer, + css: '.zammad-chat-is-open', + timeout: 4, + ) + end + + def test_timeouts + chat_url = "#{browser_url}/assets/chat/znuny-no-jquery.html?port=#{ENV['WS_PORT']}" + agent = browser_instance + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + agent.find_elements(css: '.active .chat-window .js-disconnect:not(.is-hidden)').each(&:click) + agent.find_elements(css: '.active .chat-window .js-close').each(&:click) + + exists( + browser: agent, + css: '#navigation .js-chatMenuItem .js-switch input[checked]' + ) + + # no customer action, hide widget + customer = browser_instance + location( + browser: customer, + url: chat_url, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + timeout: 5, + ) + watch_for_disappear( + browser: customer, + css: '.zammad-chat', + timeout: 95, + ) + + # no agent action, show sorry screen + reload( + browser: customer, + ) + exists( + browser: customer, + css: '.zammad-chat', + ) + click( + browser: customer, + css: '.js-chat-open', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + timeout: 35, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(takes longer|dauert länger)', + timeout: 120, + ) + + # check if agent is offline, idle timeout, chat not answered + exists_not( + browser: agent, + css: '#navigation .js-chatMenuItem .js-switch input[checked]' + ) + switch( + browser: agent, + css: '#navigation .js-chatMenuItem .js-switch', + type: 'on', + ) + + # no customer action, show sorry screen + reload( + browser: customer, + ) + exists( + browser: customer, + css: '.zammad-chat', + ) + click( + browser: customer, + css: '.js-chat-open', + ) + watch_for( + browser: agent, + css: '.js-chatMenuItem .counter', + value: '1', + ) + click( + browser: agent, + css: '.active .js-acceptChat', + ) + sleep 2 + set( + browser: agent, + css: '.active .chat-window .js-customerChatInput', + value: 'agent is asking', + ) + click( + browser: agent, + css: '.active .chat-window .js-send', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'agent is asking', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(Since you didn\'t respond|Da Sie in den letzten)', + timeout: 150, + ) + + agent.find_elements( { css: '.active .chat-window .js-close' } ).each(&:click) + sleep 2 + click( + browser: customer, + css: '.js-restart', + ) + sleep 5 + click( + browser: customer, + css: '.js-chat-open', + ) + exists( + browser: customer, + css: '.zammad-chat-is-shown', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + click( + browser: agent, + css: '.active .js-acceptChat', + ) + sleep 2 + exists( + browser: agent, + css: '.active .chat-window .chat-status', + ) + set( + browser: agent, + css: '.active .chat-window .js-customerChatInput', + value: 'my name is me', + ) + click( + browser: agent, + css: '.active .chat-window .js-send', + ) + watch_for( + browser: customer, + css: '.zammad-chat .zammad-chat-agent-status', + value: 'online', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'my name is me', + ) + + end + + def disable_chat + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + + # disable chat + click( + browser: agent, + css: 'a[href="#manage"]', + ) + click( + browser: agent, + css: '.content.active a[href="#channels/chat"]', + ) + switch( + browser: agent, + css: '.content.active .js-chatSetting', + type: 'off', + ) + end + +end