do($ = window.jQuery, 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 Base defaults: debug: false constructor: (options) -> @options = $.extend {}, @defaults, options @log = new Log(debug: @options.debug, logPrefix: @options.logPrefix || @logPrefix) class Log defaults: debug: false constructor: (options) -> @options = $.extend {}, @defaults, options 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 $('.js-chatLogDisplay').prepend('
' + logString + '
') class Timeout extends Base timeoutStartedAt: null logPrefix: 'timeout' defaults: debug: false timeout: 4 timeoutIntervallCheck: 0.5 constructor: (options) -> super(options) 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' constructor: (options) -> super(options) 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: $('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 # Callbacks onReady: undefined onCloseAnimationEnd: undefined onError: undefined onOpenAnimationEnd: undefined onConnectionReestablished: undefined onSessionClosed: undefined onConnectionEstablished: undefined onCssLoaded: undefined logPrefix: 'chat' _messageCount: 0 isOpen: false blinkOnlineInterval: null stopBlinOnlineStateTimeout: null showTimeEveryXMinutes: 2 lastTimestamp: null lastAddedType: null inputDisabled: false inputTimeout: null isTyping: false state: 'offline' initialQueueDelay: 10000 translations: # ZAMMAD_TRANSLATIONS_START 'cs': 'Chat with us!': 'Chatujte s námi!' 'All colleagues are busy.': 'Všichni kolegové jsou vytíženi.' 'Chat closed by %s': '%s ukončil konverzaci' 'Compose your message…': 'Napište svou zprávu…' 'Connecting': 'Připojování' 'Connection lost': 'Připojení ztraceno' 'Connection re-established': 'Připojení obnoveno' 'Offline': 'Offline' 'Online': 'Online' 'Scroll down to see new messages': 'Srolujte dolů pro zobrazení nových zpráv' 'Send': 'Odeslat' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Jelikož jste nereagovali v posledních %s minutách, vaše konverzace byla uzavřena.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Jelikož jste nereagovali v posledních %s minutách, vaše konverzace s %s byla uzavřena.' 'Start new conversation': 'Zahájit novou konverzaci' 'Today': 'Dnes' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Omlouváme se, že musíte čekat déle, než je vhodné pro získání slotu. Prosím, zkuste to později, případně nám napište e-mail. Děkujeme!' 'You are on waiting list position %s.': 'Jste %s. v pořadí na čekací listině.' 'da': 'Chat with us!': 'Chat med os!' 'All colleagues are busy.': 'Alle medarbejdere er optaget.' 'Chat closed by %s': 'Chat lukket af %s' 'Compose your message…': 'Skriv din besked…' 'Connecting': 'Forbinder' 'Connection lost': 'Forbindelse mistet' 'Connection re-established': 'Forbindelse genoprettet' 'Offline': 'Offline' 'Online': 'Online' 'Scroll down to see new messages': 'Scroll ned for at se nye beskeder' 'Send': 'Afsend' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': '' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': '' 'Start new conversation': 'Start en ny samtale' 'Today': 'I dag' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': '' 'You are on waiting list position %s.': 'Du er i kø som nummer %s.' 'de': 'Chat with us!': 'Chatte mit uns!' 'All colleagues are busy.': 'Alle Kollegen sind beschäftigt.' 'Chat closed by %s': 'Chat von %s geschlossen' 'Compose your message…': 'Verfassen Sie Ihre Nachricht…' 'Connecting': 'Verbinde' 'Connection lost': 'Verbindung verloren' 'Connection re-established': 'Verbindung wieder aufgebaut' 'Offline': 'Offline' 'Online': 'Online' 'Scroll down to see new messages': 'Nach unten scrollen um neue Nachrichten zu sehen' 'Send': 'Senden' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Da Sie innerhalb der letzten %s Minuten nicht reagiert haben, wurde Ihre Unterhaltung geschlossen.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Da Sie innerhalb der letzten %s Minuten nicht reagiert haben, wurde Ihre Unterhaltung mit %s geschlossen.' 'Start new conversation': 'Neue Unterhaltung starten' 'Today': 'Heute' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Entschuldigung, es dauert länger als erwartet einen freien Platz zu bekommen. Versuchen Sie es später erneut oder senden Sie uns eine E-Mail. Vielen Dank!' 'You are on waiting list position %s.': 'Sie sind in der Warteliste auf Position %s.' 'es': 'Chat with us!': 'Chatee con nosotros!' 'All colleagues are busy.': 'Todos los colegas están ocupados.' 'Chat closed by %s': 'Chat cerrado por %s' 'Compose your message…': 'Escribe tu mensaje…' 'Connecting': 'Conectando' 'Connection lost': 'Conexión perdida' 'Connection re-established': 'Conexión reestablecida' 'Offline': 'Desconectado' 'Online': 'En línea' 'Scroll down to see new messages': 'Desplace hacia abajo para ver nuevos mensajes' 'Send': 'Enviar' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Debido a que usted no ha respondido en los últimos %s minutos, su conversación se ha cerrado.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Debido a que usted no ha respondido en los últimos %s minutos, su conversación con %s se ha cerrado.' 'Start new conversation': 'Iniciar nueva conversación' 'Today': 'Hoy' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Lo sentimos, estamos tardando más de lo esperado para asignar un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!' 'You are on waiting list position %s.': 'Usted está en la posición %s de la lista de espera.' 'fr': 'Chat with us!': 'Chattez avec nous !' 'All colleagues are busy.': 'Tous les agents sont occupés.' 'Chat closed by %s': 'Chat fermé par %s' 'Compose your message…': 'Écrivez votre message…' 'Connecting': 'Connexion' 'Connection lost': 'Connexion perdue' 'Connection re-established': 'Connexion ré-établie' 'Offline': 'Hors-ligne' 'Online': 'En ligne' 'Scroll down to see new messages': 'Défiler vers le bas pour voir les nouveaux messages' 'Send': 'Envoyer' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Sans réponse de votre part depuis %s minutes, votre conservation a été fermée.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Sans réponse de votre part depuis %s minutes, votre conversation avec %s a été fermée.' 'Start new conversation': 'Démarrer une nouvelle conversation' 'Today': 'Aujourd\'hui' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Nous sommes désolés, trouver un agent disponible prend plus de temps que prévu. Réessayez plus tard ou envoyez-nous un mail. Merci !' 'You are on waiting list position %s.': 'Vous êtes actuellement en position %s dans la file d\'attente.' 'hr': 'Chat with us!': 'Čavrljajte sa nama!' 'All colleagues are busy.': 'Svi kolege su zauzeti.' 'Chat closed by %s': '%s zatvara chat' 'Compose your message…': 'Sastavite poruku…' 'Connecting': 'Povezivanje' 'Connection lost': 'Veza prekinuta' 'Connection re-established': 'Veza je ponovno uspostavljena' 'Offline': 'Odsutan' 'Online': 'Dostupan(a)' 'Scroll down to see new messages': 'Pomaknite se prema dolje da biste vidjeli nove poruke' 'Send': 'Šalji' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Budući da niste odgovorili u posljednjih %s minuta, Vaš je razgovor zatvoren.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Budući da niste odgovorili u posljednjih %s minuta, Vaš je razgovor s %s zatvoren.' 'Start new conversation': 'Započni novi razgovor' 'Today': 'Danas' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Oprostite, proces traje duže nego što se očekivalo da biste dobili slobodan termin. Molimo, pokušajte ponovno kasnije ili nam pošaljite e-mail. Hvala!' 'You are on waiting list position %s.': 'Nalazite se u redu čekanja na poziciji %s.' 'hu': 'Chat with us!': 'Csevegjen velünk!' 'All colleagues are busy.': 'Minden munkatársunk foglalt.' 'Chat closed by %s': 'A csevegés %s által lezárva' 'Compose your message…': 'Fogalmazza meg üzenetét…' 'Connecting': 'Csatlakozás' 'Connection lost': 'A kapcsolat megszakadt' 'Connection re-established': 'A kapcsolat helyreállt' 'Offline': 'Offline' 'Online': 'Online' 'Scroll down to see new messages': 'Görgessen lefelé az új üzenetek megtekintéséhez' 'Send': 'Küldés' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Mivel az elmúlt %s percben nem válaszolt, a beszélgetése lezárásra került.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Mivel az elmúlt %s percben nem válaszolt, %s munkatársunkkal folytatott beszélgetését lezártuk.' 'Start new conversation': 'Új beszélgetés indítása' 'Today': 'Ma' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Sajnáljuk, hogy a vártnál hosszabb ideig tart a helyfoglalás. Kérjük, próbálja meg később újra, vagy küldjön nekünk egy e-mailt. Köszönjük!' 'You are on waiting list position %s.': 'Ön a várólistán a %s helyen szerepel.' 'it': 'Chat with us!': 'Chatta con noi!' 'All colleagues are busy.': 'Tutti i colleghi sono occupati.' 'Chat closed by %s': 'Chat chiusa da %s' 'Compose your message…': 'Scrivi il tuo messaggio…' 'Connecting': 'Connessione in corso' 'Connection lost': 'Connessione persa' 'Connection re-established': 'Connessione ristabilita' 'Offline': 'Offline' 'Online': 'Online' 'Scroll down to see new messages': 'Scorri verso il basso per vedere i nuovi messaggi' 'Send': 'Invia' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Dato che non hai risposto negli ultimi %s minuti, la conversazione è stata chiusa.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Dato che non hai risposto negli ultimi %s minuti, la conversazione con %s è stata chiusa.' 'Start new conversation': 'Avvia una nuova chat' 'Today': 'Oggi' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Siamo spiacenti, ci vuole più tempo del previsto per ottenere uno spazio libero. Riprova più tardi o inviaci un\'e-mail. Grazie!' 'You are on waiting list position %s.': 'Sei alla posizione %s della lista di attesa.' 'lt': 'Chat with us!': 'Kalbėkitės su mumis!' 'All colleagues are busy.': 'Visi kolegos užimti.' 'Chat closed by %s': '%s uždarė pokalbį' 'Compose your message…': 'Rašykite žinutę…' 'Connecting': 'Jungiamasi' 'Connection lost': 'Dingo ryšys' 'Connection re-established': 'Ryšys atnaujintas' 'Offline': 'Atsijungęs' 'Online': 'Prisijungęs' 'Scroll down to see new messages': 'Naujos žinutės žemiau' 'Send': 'Siųsti' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Jūsų pokalbis buvo uždarytas, nes nieko neatsakėte per %s minučių.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Jūsų pokalbis su %s buvo uždarytas, nes nieko neatsakėte per %s minučių.' 'Start new conversation': 'Pradėti naują pokalbį' 'Today': 'Šiandien' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Atsiprašome, kad tenka laukti atskymo. Bandykite vėliau arba rašykite el. paštu. Ačiū!' 'You are on waiting list position %s.': 'Esate %s eilėje.' 'nl': 'Chat with us!': 'Chat met ons!' 'All colleagues are busy.': 'Alle collega\'s zijn bezet.' 'Chat closed by %s': 'Chat gesloten door %s' 'Compose your message…': 'Stel je bericht op…' 'Connecting': 'Verbinden' 'Connection lost': 'Verbinding verbroken' 'Connection re-established': 'Verbinding hersteld' 'Offline': 'Offline' 'Online': 'Online' 'Scroll down to see new messages': 'Scroll naar beneden om nieuwe tickets te bekijken' 'Send': 'Verstuur' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'De chat is afgesloten omdat je de laatste %s minuten niet hebt gereageerd.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Je chat met %s is afgesloten omdat je niet hebt gereageerd in de laatste %s minuten.' 'Start new conversation': 'Nieuw gesprek starten' 'Today': 'Vandaag' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Het spijt ons, het duurt langer dan verwacht om een chat te starten. Probeer het later nog eens of stuur ons een e-mail. Bedankt!' 'You are on waiting list position %s.': 'Je bevindt zich op wachtlijstpositie %s.' 'pl': 'Chat with us!': 'Czatuj z nami!' 'All colleagues are busy.': 'Wszyscy agenci są zajęci.' 'Chat closed by %s': 'Chat zamknięty przez %s' 'Compose your message…': 'Skomponuj swoją wiadomość…' 'Connecting': 'Łączenie' 'Connection lost': 'Utracono połączenie' 'Connection re-established': 'Ponowne nawiązanie połączenia' 'Offline': 'Offline' 'Online': 'Online' 'Scroll down to see new messages': 'Skroluj w dół, aby zobaczyć wiadomości' 'Send': 'Wyślij' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa z %s została zamknięta.' 'Start new conversation': 'Rozpocznij nową rozmowę' 'Today': 'Dzisiaj' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Przepraszamy, znalezienie wolnego konsultanta zajmuje więcej czasu niż oczekiwano. Spróbuj ponownie później lub wyślij nam e-mail. Dziękujemy!' 'You are on waiting list position %s.': 'Jesteś na pozycji listy oczekujących %s.' 'pt-br': 'Chat with us!': 'Converse conosco!' 'All colleagues are busy.': 'Nossos atendentes estão ocupados.' 'Chat closed by %s': 'Chat encerrado por %s' 'Compose your message…': 'Escreva sua mensagem…' 'Connecting': 'Conectando' 'Connection lost': 'Conexão perdida' 'Connection re-established': 'Conexão restabelecida' 'Offline': 'Desconectado' 'Online': 'Online' 'Scroll down to see new messages': 'Rolar para baixo para ver novas mensagems' 'Send': 'Enviar' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Como você não respondeu nos últimos %s minutos, sua conversa foi encerrada.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Como você não respondeu nos últimos %s minutos, sua conversa com %s foi encerrada.' 'Start new conversation': 'Iniciar uma nova conversa' 'Today': 'Hoje' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Lamentamos, está demorando mais do que o esperado para conseguir uma vaga. Tente novamente mais tarde ou envie-nos um e-mail. Obrigado!' 'You are on waiting list position %s.': 'Você está na posição %s da lista de espera.' 'ro': 'Chat with us!': 'Comunică cu noi!' 'All colleagues are busy.': 'Toți colegii sunt ocupați momentan.' 'Chat closed by %s': 'Chat închis de către %s' 'Compose your message…': 'Compune-ți mesajul…' 'Connecting': 'Se conectează' 'Connection lost': 'Conexiune pierdută' 'Connection re-established': 'Conexiune restabilită' 'Offline': 'Offline' 'Online': 'Online' 'Scroll down to see new messages': 'Derulați în jos pentru a vedea mesajele noi' 'Send': 'Trimite' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Deoarece nu ai răspuns în ultimele %s minute, conversația ta a fost închisă.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Deoarece nu ai răspuns în ultimele %s minute, conversația ta cu %s a fost închisă.' 'Start new conversation': 'Începe o conversație nouă' 'Today': 'Azi' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Ne pare rău, durează mai mult decât ne așteptam să obținem un loc. Te rugăm să încerci din nou mai târziu sau să ne trimiți un email. Mulțumim!' 'You are on waiting list position %s.': 'Aveți poziția %s în lista de așteptare.' 'ru': 'Chat with us!': 'Напишите нам!' 'All colleagues are busy.': 'Все коллеги заняты.' 'Chat closed by %s': 'Чат закрыт %s' 'Compose your message…': 'Составьте сообщение…' 'Connecting': 'Подключение' 'Connection lost': 'Подключение потеряно' 'Connection re-established': 'Подключение восстановлено' 'Offline': 'Оффлайн' 'Online': 'В сети' 'Scroll down to see new messages': 'Прокрутите вниз, чтобы увидеть новые сообщения' 'Send': 'Отправить' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Поскольку Вы не ответили в течение последних %s минут, Ваш разговор был закрыт.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Поскольку Вы не ответили в течение последних %s минут, Ваш разговор с %s был закрыт.' 'Start new conversation': 'Начать новый разговор' 'Today': 'Сегодня' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Извините, получение свободного слота занимает больше времени, чем ожидалось. Пожалуйста, повторите попытку позже или отправьте нам электронное письмо. Благодарим Вас!' 'You are on waiting list position %s.': 'Вы находитесь в списке ожидания %s.' 'sr': 'Chat with us!': 'Ћаскајте са нама!' 'All colleagues are busy.': 'Све колеге су заузете.' 'Chat closed by %s': 'Ћаскање затворено од стране %s' 'Compose your message…': 'Напишите поруку…' 'Connecting': 'Повезивање' 'Connection lost': 'Веза је изгубљена' 'Connection re-established': 'Веза је поново успостављена' 'Offline': 'Одсутан(а)' 'Online': 'Доступан(а)' 'Scroll down to see new messages': 'Скролујте на доле за нове поруке' 'Send': 'Пошаљи' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Пошто нисте одговорили у последњих %s минут(a), ваш разговор је завршен.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Пошто нисте одговорили у последњих %s минут(a), ваш разговор са %s је завршен.' 'Start new conversation': 'Започни нови разговор' 'Today': 'Данас' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Жао нам је, добијање празног термина траје дуже од очекиваног. Молимо покушајте поново касније или нам пошаљите имејл поруку. Хвала вам!' 'You are on waiting list position %s.': 'Ви сте тренутно %s. у реду за чекање.' 'sr-latn-rs': 'Chat with us!': 'Ćaskajte sa nama!' 'All colleagues are busy.': 'Sve kolege su zauzete.' 'Chat closed by %s': 'Ćaskanje zatvoreno od strane %s' 'Compose your message…': 'Napišite poruku…' 'Connecting': 'Povezivanje' 'Connection lost': 'Veza je izgubljena' 'Connection re-established': 'Veza je ponovo uspostavljena' 'Offline': 'Odsutan(a)' 'Online': 'Dostupan(a)' 'Scroll down to see new messages': 'Skrolujte na dole za nove poruke' 'Send': 'Pošalji' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Pošto niste odgovorili u poslednjih %s minut(a), vaš razgovor je završen.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Pošto niste odgovorili u poslednjih %s minut(a), vaš razgovor sa %s je završen.' 'Start new conversation': 'Započni novi razgovor' 'Today': 'Danas' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Žao nam je, dobijanje praznog termina traje duže od očekivanog. Molimo pokušajte ponovo kasnije ili nam pošaljite imejl poruku. Hvala vam!' 'You are on waiting list position %s.': 'Vi ste trenutno %s. u redu za čekanje.' 'sv': 'Chat with us!': 'Chatta med oss!' 'All colleagues are busy.': 'Alla kollegor är upptagna.' 'Chat closed by %s': 'Chatt stängd av %s' 'Compose your message…': 'Skriv ditt meddelande …' 'Connecting': 'Ansluter' 'Connection lost': 'Anslutningen försvann' 'Connection re-established': 'Anslutningen återupprättas' 'Offline': 'Offline' 'Online': 'Online' 'Scroll down to see new messages': 'Bläddra ner för att se nya meddelanden' 'Send': 'Skicka' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Din chatt avslutades då du inte svarade inom %s minuter.' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': 'Chatten stängdes eftersom du inte svarat inom %s minuter i din konversation med %s.' 'Start new conversation': 'Starta ny konversation' 'Today': 'Idag' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Det tar tyvärr längre tid än förväntat att få en ledig plats. Försök igen senare eller skicka ett mejl till oss. Tack!' 'You are on waiting list position %s.': 'Du är på väntelistan som position %s.' 'zh-cn': 'Chat with us!': '发起即时对话!' 'All colleagues are busy.': '所有同事都很忙。' 'Chat closed by %s': '对话已被 %s 关闭' 'Compose your message…': '编辑您的信息…' 'Connecting': '连接中' 'Connection lost': '连接丢失' 'Connection re-established': '正在重新建立连接' 'Offline': '离线' 'Online': '在线' 'Scroll down to see new messages': '向下滚动以查看新消息' 'Send': '发送' 'Since you didn\'t respond in the last %s minutes your conversation was closed.': '"由于您超过 %s 分钟没有任何回复' 'Since you didn\'t respond in the last %s minutes your conversation with %s was closed.': '"由于您超过 %s 分钟没有回复' 'Start new conversation': '开始新的会话' 'Today': '今天' 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': '' 'You are on waiting list position %s.': '您目前的等候位置是第 %s 位.' # ZAMMAD_TRANSLATIONS_END 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) -> @options = $.extend {}, @defaults, options super(@options) # fullscreen @isFullscreen = (window.matchMedia and window.matchMedia('(max-width: 768px)').matches) @scrollRoot = $(@getScrollRoot()) # check prerequisites if !$ @state = 'unsupported' @log.notice 'Chat: no jquery found!' return 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 = $('html').attr('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 = html.scrollTop html.scrollTop = start + 1 end = html.scrollTop html.scrollTop = start return if end > start then html else document.body render: => if !@el || !$('.zammad-chat').get(0) @renderBase() # disable open button $(".#{ @options.buttonClass }").addClass @options.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 = $(@view('chat')( title: @options.title, scrollHint: @options.scrollHint )) @options.target.append @el @input = @el.find('.zammad-chat-input') # start bindings @el.find('.js-chat-open').on 'click', @open @el.find('.js-chat-toggle').on 'click', @toggle @el.find('.js-chat-status').on 'click', @stopPropagation @el.find('.zammad-chat-controls').on 'submit', @onSubmit @el.find('.zammad-chat-body').on 'scroll', @detectScrolledtoBottom @el.find('.zammad-scroll-hint').on 'click', @onScrollHintClick @input.on( keydown: @checkForEnter input: @onInput ) @input.on('keydown', (e) => 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 ) @input.on('paste', (e) => e.stopPropagation() e.preventDefault() clipboardData if e.clipboardData clipboardData = e.clipboardData else if window.clipboardData clipboardData = window.clipboardData else if e.originalEvent.clipboardData clipboardData = e.originalEvent.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) => result = e.target.result img = document.createElement('img') img.src = result insert = (dataUrl, width, height, isRetina) => # adapt image if we are on retina devices if @isRetina() width = width / 2 height = height / 2 result = dataUrl img = "" document.execCommand('insertHTML', false, img) # resize if to big @resizeImage(img.src, 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' sanitized = DOMPurify.sanitize(text) @log.debug 'sanitized HTML clipboard', sanitized html = $("
#{sanitized}
") 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 html = $(html) html.contents().each( -> if @nodeType == 8 $(@).remove() ) # remove tags, keep content html.find('a, font, small, time, form, label').replaceWith( -> $(@).contents() ) # replace tags with generic div # New type of the tag replacementTag = 'div'; # Replace all x tags with the type of replacementTag html.find('textarea').each( -> outer = @outerHTML # Replace opening tag regex = new RegExp('<' + @tagName, 'i') newTag = outer.replace(regex, '<' + replacementTag) # Replace closing tag regex = new RegExp(' e.stopPropagation() e.preventDefault() dataTransfer if window.dataTransfer # ie dataTransfer = window.dataTransfer else if e.originalEvent.dataTransfer # other browsers dataTransfer = e.originalEvent.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) => result = e.target.result img = document.createElement('img') img.src = result # Insert the image at the carat insert = (dataUrl, width, height, isRetina) => # adapt image if we are on retina devices if @isRetina() width = width / 2 height = height / 2 result = dataUrl img = $("") img = img.get(0) 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(img.src, 460, 'auto', 2, 'image/jpeg', 'auto', insert) reader.readAsDataURL(file) ) $(window).on('beforeunload', => @onLeaveTemporary() ) $(window).on('hashchange', => if @isOpen if @sessionId @send 'chat_session_notice', session_id: @sessionId message: window.location.href return @idleTimeout.start() ) if @isFullscreen @input.on focus: @onFocus focusout: @onFocusOut stopPropagation: (event) -> event.stopPropagation() checkForEnter: (event) => if not @inputDisabled and not event.shiftKey and event.keyCode is 13 event.preventDefault() @sendMessage() 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_session_notice' @addStatus @T(pipe.data.message) 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' $(".#{ @options.buttonClass }").on('click', @open).removeClass(@options.inactiveClass) @options.onReady?() if @options.show @show() onError: (message) => @log.debug message @addStatus(message) $(".#{ @options.buttonClass }").hide() if @isOpen @disableInput() @destroy(remove: false) else @destroy(remove: true) @options.onError?(message) 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.html(unfinishedMessage) # show wait list if data.position @onQueue data @show() @open() @scrollToBottom() if unfinishedMessage @input.trigger('focus') onInput: => # remove unread-state from messages @el.find('.zammad-chat-message--unread') .removeClass 'zammad-chat-message--unread' sessionStorage.setItem 'unfinished_message', @input.html() @onTyping() onFocus: => $(window).scrollTop(10) keyboardShown = $(window).scrollTop() > 0 $(window).scrollTop(0) if keyboardShown @log.notice 'virtual keyboard shown' # on keyboard shown # can't measure visible area height :( onFocusOut: -> # on keyboard hidden 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.html() 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.find('.zammad-chat-message--typing').get(0) @lastAddedType = 'typing-placeholder' @el.find('.zammad-chat-message--typing').before messageElement else @lastAddedType = 'message--customer' @el.find('.zammad-chat-body').append messageElement @input.html('') @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 '' @el.find('.zammad-chat-body').append @view('message')(data) open: => if @isOpen @log.debug 'widget already open, block' return @isOpen = true @log.debug 'open widget' @show() if !@sessionId @showLoader() @el.addClass('zammad-chat-is-open') remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight() @el.css 'bottom', -remainerHeight if !@sessionId @el.animate { bottom: 0 }, 500, @onOpenAnimationEnd @send('chat_session_init' url: window.location.href ) else @el.css 'bottom', 0 @onOpenAnimationEnd() onOpenAnimationEnd: => @idleTimeout.stop() if @isFullscreen @disableScrollOnRoot() @options.onOpenAnimationEnd?() 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 'session close before widget close' @sessionClose() @log.debug 'close widget' event.stopPropagation() if event if @isFullscreen @enableScrollOnRoot() # close window remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight() @el.animate { bottom: -remainerHeight }, 500, @onCloseAnimationEnd onCloseAnimationEnd: => @el.css 'bottom', '' @el.removeClass('zammad-chat-is-open') @showLoader() @el.find('.zammad-chat-welcome').removeClass('zammad-chat-is-hidden') @el.find('.zammad-chat-agent').addClass('zammad-chat-is-hidden') @el.find('.zammad-chat-agent-status').addClass('zammad-chat-is-hidden') @isOpen = false @options.onCloseAnimationEnd?() @io.reconnect() onWebSocketClose: => return if @isOpen if @el @el.removeClass('zammad-chat-is-shown') @el.removeClass('zammad-chat-is-loaded') show: -> return if @state is 'offline' @el.addClass('zammad-chat-is-loaded') @el.addClass('zammad-chat-is-shown') disableInput: -> @inputDisabled = true @input.prop('contenteditable', false) @el.find('.zammad-chat-send').prop('disabled', true) @io.close() enableInput: -> @inputDisabled = false @input.prop('contenteditable', true) @el.find('.zammad-chat-send').prop('disabled', false) hideModal: -> @el.find('.zammad-chat-modal').html '' 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.find('.zammad-chat-modal').html @view('waiting') position: data.position onAgentTypingStart: => if @stopTypingId clearTimeout(@stopTypingId) @stopTypingId = setTimeout(@onAgentTypingEnd, 3000) # never display two typing indicators return if @el.find('.zammad-chat-message--typing').get(0) @maybeAddTimestamp() @el.find('.zammad-chat-body').append @view('typingIndicator')() # only if typing indicator is shown return if !@isVisible(@el.find('.zammad-chat-message--typing'), true) @scrollToBottom() onAgentTypingEnd: => @el.find('.zammad-chat-message--typing').remove() 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 @el.find('.zammad-chat-body').append @view('timestamp') label: label time: time @lastTimestamp = timestamp @lastAddedType = 'timestamp' @scrollToBottom() updateLastTimestamp: (label, time) -> return if !@el @el.find('.zammad-chat-body') .find('.zammad-chat-timestamp') .last() .replaceWith @view('timestamp') label: label time: time addStatus: (status) -> return if !@el @maybeAddTimestamp() @el.find('.zammad-chat-body').append @view('status') status: status @scrollToBottom() detectScrolledtoBottom: => scrollBottom = @el.find('.zammad-chat-body').scrollTop() + @el.find('.zammad-chat-body').outerHeight() @scrolledToBottom = Math.abs(scrollBottom - @el.find('.zammad-chat-body').prop('scrollHeight')) <= @scrollSnapTolerance @el.find('.zammad-scroll-hint').addClass('is-hidden') if @scrolledToBottom showScrollHint: -> @el.find('.zammad-scroll-hint').removeClass('is-hidden') # compensate scroll @el.find('.zammad-chat-body').scrollTop(@el.find('.zammad-chat-body').scrollTop() + @el.find('.zammad-scroll-hint').outerHeight()) onScrollHintClick: => # animate scroll @el.find('.zammad-chat-body').animate({scrollTop: @el.find('.zammad-chat-body').prop('scrollHeight')}, 300) scrollToBottom: ({ showHint } = { showHint: false }) -> if @scrolledToBottom @el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight')) else if showHint @showScrollHint() destroy: (params = {}) => @log.debug 'destroy widget', params @setAgentOnlineState 'offline' if params.remove && @el @el.remove() # Remove button, because it can no longer be used. $(".#{ @options.buttonClass }").hide() # 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') @options.onConnectionReestablished?() onSessionClosed: (data) -> @addStatus @T('Chat closed by %s', data.realname) @disableInput() @setAgentOnlineState 'offline' @inactiveTimeout.stop() @options.onSessionClosed?(data) 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 @el.find('.zammad-chat-body').html('') @el.find('.zammad-chat-agent').html @view('agent') agent: @agent @enableInput() @hideModal() @el.find('.zammad-chat-welcome').addClass('zammad-chat-is-hidden') @el.find('.zammad-chat-agent').removeClass('zammad-chat-is-hidden') @el.find('.zammad-chat-agent-status').removeClass('zammad-chat-is-hidden') @input.trigger('focus') if not @isFullscreen @setAgentOnlineState 'online' @waitingListTimeout.stop() @idleTimeout.stop() @inactiveTimeout.start() @options.onConnectionEstablished?(data) showCustomerTimeout: -> @el.find('.zammad-chat-modal').html @view('customer_timeout') agent: @agent.name delay: @options.inactiveTimeout reload = -> location.reload() @el.find('.js-restart').on 'click', reload @sessionClose() showWaitingListTimeout: -> @el.find('.zammad-chat-modal').html @view('waiting_list_timeout') delay: @options.watingListTimeout reload = -> location.reload() @el.find('.js-restart').on 'click', reload @sessionClose() showLoader: -> @el.find('.zammad-chat-modal').html @view('loader')() setAgentOnlineState: (state) => @state = state return if !@el capitalizedState = state.charAt(0).toUpperCase() + state.slice(1) @el .find('.zammad-chat-agent-status') .attr('data-status', state) .text @T(capitalizedState) # @T('Online') @T('Offline') 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, '') # WebSocket may run on example.com/ws path 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() @options.onCssLoaded?() 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.css overflow: 'hidden' position: 'fixed' enableScrollOnRoot: -> @scrollRoot.scrollTop @rootScrollOffset @scrollRoot.css overflow: '' 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 $w = $(window) $t = if el.length > 1 then el.eq(0) else el t = $t.get(0) vpWidth = $w.width() vpHeight = $w.height() direction = if direction then direction else 'both' clientSize = if hidden is true then t.offsetWidth * t.offsetHeight else true if typeof t.getBoundingClientRect is 'function' # Use this native browser method, if available. rec = t.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 else viewTop = $w.scrollTop() viewBottom = viewTop + vpHeight viewLeft = $w.scrollLeft() viewRight = viewLeft + vpWidth offset = $t.offset() _top = offset.top _bottom = _top + $t.height() _left = offset.left _right = _left + $t.width() compareTop = if partial is true then _bottom else _top compareBottom = if partial is true then _top else _bottom compareLeft = if partial is true then _right else _left compareRight = if partial is true then _left else _right if direction is 'both' return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop)) && ((compareRight <= viewRight) && (compareLeft >= viewLeft)) else if direction is 'vertical' return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop)) else if direction is 'horizontal' return !!clientSize && ((compareRight <= viewRight) && (compareLeft >= viewLeft)) 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.html(content) # Parse out list indent level for lists $('p', editor).each( -> str = $(@).attr('style') matches = /mso-list:\w+ \w+([0-9]+)/.exec(str) if matches $(@).data('_listLevel', parseInt(matches[1], 10)) ) # Parse Lists last_level = 0 pnt = null $('p', editor).each(-> cur_level = $(@).data('_listLevel') if cur_level != undefined txt = $(@).text() 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 $(@).before(list_tag) pnt = $(@).prev() else pnt = $(list_tag).appendTo(pnt) if cur_level < last_level for i in [i..last_level-cur_level] pnt = pnt.parent() $('span:first', @).remove() pnt.append('
        1. ' + $(@).html() + '
        2. ') $(@).remove() last_level = cur_level else last_level = 0 ) $('[style]', editor).removeAttr('style') $('[align]', editor).removeAttr('align') $('span', editor).replaceWith(-> $(@).contents() ) $('span:empty', editor).remove() $("[class^='Mso']", editor).removeAttr('class') $('p:empty', editor).remove() editor removeAttribute: (element) -> return if !element $element = $(element) for att in element.attributes if att && att.name element.removeAttribute(att.name) #$element.removeAttr(att.name) $element.removeAttr('style') .removeAttr('class') .removeAttr('lang') .removeAttr('type') .removeAttr('align') .removeAttr('id') .removeAttr('wrap') .removeAttr('title') removeAttributes: (html, parent = true) => if parent html.each((index, element) => @removeAttribute(element) ) html.find('*').each((index, element) => @removeAttribute(element) ) html window.ZammadChat = ZammadChat