chat-no-jquery.coffee 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543
  1. do(window) ->
  2. scripts = document.getElementsByTagName('script')
  3. # search for script to get protocol and hostname for ws connection
  4. myScript = scripts[scripts.length - 1]
  5. scriptProtocol = window.location.protocol.replace(':', '') # set default protocol
  6. if myScript && myScript.src
  7. scriptHost = myScript.src.match('.*://([^:/]*).*')[1]
  8. scriptProtocol = myScript.src.match('(.*)://[^:/]*.*')[1]
  9. # Define the plugin class
  10. class Core
  11. defaults:
  12. debug: false
  13. constructor: (options) ->
  14. @options = {}
  15. for key, value of @defaults
  16. @options[key] = value
  17. for key, value of options
  18. @options[key] = value
  19. class Base extends Core
  20. constructor: (options) ->
  21. super(options)
  22. @log = new Log(debug: @options.debug, logPrefix: @options.logPrefix || @logPrefix)
  23. class Log extends Core
  24. debug: (items...) =>
  25. return if !@options.debug
  26. @log('debug', items)
  27. notice: (items...) =>
  28. @log('notice', items)
  29. error: (items...) =>
  30. @log('error', items)
  31. log: (level, items) =>
  32. items.unshift('||')
  33. items.unshift(level)
  34. items.unshift(@options.logPrefix)
  35. console.log.apply console, items
  36. return if !@options.debug
  37. logString = ''
  38. for item in items
  39. logString += ' '
  40. if typeof item is 'object'
  41. logString += JSON.stringify(item)
  42. else if item && item.toString
  43. logString += item.toString()
  44. else
  45. logString += item
  46. element = document.querySelector('.js-chatLogDisplay')
  47. if element
  48. element.innerHTML = '<div>' + logString + '</div>' + element.innerHTML
  49. class Timeout extends Base
  50. timeoutStartedAt: null
  51. logPrefix: 'timeout'
  52. defaults:
  53. debug: false
  54. timeout: 4
  55. timeoutIntervallCheck: 0.5
  56. start: =>
  57. @stop()
  58. timeoutStartedAt = new Date
  59. check = =>
  60. timeLeft = new Date - new Date(timeoutStartedAt.getTime() + @options.timeout * 1000 * 60)
  61. @log.debug "Timeout check for #{@options.timeout} minutes (left #{timeLeft/1000} sec.)"#, new Date
  62. return if timeLeft < 0
  63. @stop()
  64. @options.callback()
  65. @log.debug "Start timeout in #{@options.timeout} minutes"#, new Date
  66. @intervallId = setInterval(check, @options.timeoutIntervallCheck * 1000 * 60)
  67. stop: =>
  68. return if !@intervallId
  69. @log.debug "Stop timeout of #{@options.timeout} minutes"#, new Date
  70. clearInterval(@intervallId)
  71. class Io extends Base
  72. logPrefix: 'io'
  73. set: (params) =>
  74. for key, value of params
  75. @options[key] = value
  76. connect: =>
  77. @log.debug "Connecting to #{@options.host}"
  78. @ws = new window.WebSocket("#{@options.host}")
  79. @ws.onopen = (e) =>
  80. @log.debug 'onOpen', e
  81. @options.onOpen(e)
  82. @ping()
  83. @ws.onmessage = (e) =>
  84. pipes = JSON.parse(e.data)
  85. @log.debug 'onMessage', e.data
  86. for pipe in pipes
  87. if pipe.event is 'pong'
  88. @ping()
  89. if @options.onMessage
  90. @options.onMessage(pipes)
  91. @ws.onclose = (e) =>
  92. @log.debug 'close websocket connection', e
  93. if @pingDelayId
  94. clearTimeout(@pingDelayId)
  95. if @manualClose
  96. @log.debug 'manual close, onClose callback'
  97. @manualClose = false
  98. if @options.onClose
  99. @options.onClose(e)
  100. else
  101. @log.debug 'error close, onError callback'
  102. if @options.onError
  103. @options.onError('Connection lost...')
  104. @ws.onerror = (e) =>
  105. @log.debug 'onError', e
  106. if @options.onError
  107. @options.onError(e)
  108. close: =>
  109. @log.debug 'close websocket manually'
  110. @manualClose = true
  111. @ws.close()
  112. reconnect: =>
  113. @log.debug 'reconnect'
  114. @close()
  115. @connect()
  116. send: (event, data = {}) =>
  117. @log.debug 'send', event, data
  118. msg = JSON.stringify
  119. event: event
  120. data: data
  121. @ws.send msg
  122. ping: =>
  123. localPing = =>
  124. @send('ping')
  125. @pingDelayId = setTimeout(localPing, 29000)
  126. class ZammadChat extends Base
  127. defaults:
  128. chatId: undefined
  129. show: true
  130. target: document.querySelector('body')
  131. host: ''
  132. debug: false
  133. flat: false
  134. lang: undefined
  135. cssAutoload: true
  136. cssUrl: undefined
  137. fontSize: undefined
  138. buttonClass: 'open-zammad-chat'
  139. inactiveClass: 'is-inactive'
  140. title: '<strong>Chat</strong> with us!'
  141. scrollHint: 'Scroll down to see new messages'
  142. idleTimeout: 6
  143. idleTimeoutIntervallCheck: 0.5
  144. inactiveTimeout: 8
  145. inactiveTimeoutIntervallCheck: 0.5
  146. waitingListTimeout: 4
  147. waitingListTimeoutIntervallCheck: 0.5
  148. # Callbacks
  149. onReady: undefined
  150. onCloseAnimationEnd: undefined
  151. onError: undefined
  152. onOpenAnimationEnd: undefined
  153. onConnectionReestablished: undefined
  154. onSessionClosed: undefined
  155. onConnectionEstablished: undefined
  156. onCssLoaded: undefined
  157. logPrefix: 'chat'
  158. _messageCount: 0
  159. isOpen: false
  160. blinkOnlineInterval: null
  161. stopBlinOnlineStateTimeout: null
  162. showTimeEveryXMinutes: 2
  163. lastTimestamp: null
  164. lastAddedType: null
  165. inputTimeout: null
  166. isTyping: false
  167. state: 'offline'
  168. initialQueueDelay: 10000
  169. translations:
  170. 'da':
  171. '<strong>Chat</strong> with us!': '<strong>Chat</strong> med os!'
  172. 'Scroll down to see new messages': 'Scroll ned for at se nye beskeder'
  173. 'Online': 'Online'
  174. 'Offline': 'Offline'
  175. 'Connecting': 'Forbinder'
  176. 'Connection re-established': 'Forbindelse genoprettet'
  177. 'Today': 'I dag'
  178. 'Send': 'Send'
  179. 'Chat closed by %s': 'Chat lukket af %s'
  180. 'Compose your message...': 'Skriv en besked...'
  181. 'All colleagues are busy.': 'Alle kollegaer er optaget.'
  182. 'You are on waiting list position <strong>%s</strong>.': 'Du er i venteliste som nummer <strong>%s</strong>.'
  183. 'Start new conversation': 'Start en ny samtale'
  184. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Da du ikke har svaret i de sidste %s minutter er din samtale med <strong>%s</strong> blevet lukket.'
  185. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Da du ikke har svaret i de sidste %s minutter er din samtale blevet lukket.'
  186. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Vi beklager, det tager længere end forventet at få en ledig plads. Prøv venligst igen senere eller send os en e-mail. På forhånd tak!'
  187. 'de':
  188. '<strong>Chat</strong> with us!': '<strong>Chatte</strong> mit uns!'
  189. 'Scroll down to see new messages': 'Scrolle nach unten um neue Nachrichten zu sehen'
  190. 'Online': 'Online'
  191. 'Offline': 'Offline'
  192. 'Connecting': 'Verbinden'
  193. 'Connection re-established': 'Verbindung wiederhergestellt'
  194. 'Today': 'Heute'
  195. 'Send': 'Senden'
  196. 'Chat closed by %s': 'Chat beendet von %s'
  197. 'Compose your message...': 'Ihre Nachricht...'
  198. 'All colleagues are busy.': 'Alle Kollegen sind belegt.'
  199. 'You are on waiting list position <strong>%s</strong>.': 'Sie sind in der Warteliste an der Position <strong>%s</strong>.'
  200. 'Start new conversation': 'Neue Konversation starten'
  201. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit <strong>%s</strong> geschlossen.'
  202. '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.'
  203. '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!'
  204. 'es':
  205. '<strong>Chat</strong> with us!': '<strong>Chatee</strong> con nosotros!'
  206. 'Scroll down to see new messages': 'Haga scroll hacia abajo para ver nuevos mensajes'
  207. 'Online': 'En linea'
  208. 'Offline': 'Desconectado'
  209. 'Connecting': 'Conectando'
  210. 'Connection re-established': 'Conexión restablecida'
  211. 'Today': 'Hoy'
  212. 'Send': 'Enviar'
  213. 'Chat closed by %s': 'Chat cerrado por %s'
  214. 'Compose your message...': 'Escriba su mensaje...'
  215. 'All colleagues are busy.': 'Todos los agentes están ocupados.'
  216. 'You are on waiting list position <strong>%s</strong>.': 'Usted está en la posición <strong>%s</strong> de la lista de espera.'
  217. 'Start new conversation': 'Iniciar nueva conversación'
  218. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Puesto que usted no respondió en los últimos %s minutos su conversación con <strong>%s</strong> se ha cerrado.'
  219. '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.'
  220. '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!'
  221. 'fi':
  222. '<strong>Chat</strong> with us!': '<strong>Keskustele</strong> kanssamme!'
  223. 'Scroll down to see new messages': 'Rullaa alas nähdäksesi uudet viestit'
  224. 'Online': 'Paikalla'
  225. 'Offline': 'Poissa'
  226. 'Connecting': 'Yhdistetään'
  227. 'Connection re-established': 'Yhteys muodostettu uudelleen'
  228. 'Today': 'Tänään'
  229. 'Send': 'Lähetä'
  230. 'Chat closed by %s': '%s sulki keskustelun'
  231. 'Compose your message...': 'Luo viestisi...'
  232. 'All colleagues are busy.': 'Kaikki kollegat ovat varattuja.'
  233. 'You are on waiting list position <strong>%s</strong>.': 'Olet odotuslistalla sijalla <strong>%s</strong>.'
  234. 'Start new conversation': 'Aloita uusi keskustelu'
  235. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Koska et vastannut viimeiseen %s minuuttiin, keskustelusi <strong>%s</strong> kanssa suljettiin.'
  236. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Koska et vastannut viimeiseen %s minuuttiin, keskustelusi suljettiin.'
  237. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Olemme pahoillamme, tyhjän paikan vapautumisessa kestää odotettua pidempään. Ole hyvä ja yritä myöhemmin uudestaan tai lähetä meille sähköpostia. Kiitos!'
  238. 'fr':
  239. '<strong>Chat</strong> with us!': '<strong>Chattez</strong> avec nous!'
  240. 'Scroll down to see new messages': 'Faites défiler pour lire les nouveaux messages'
  241. 'Online': 'En-ligne'
  242. 'Offline': 'Hors-ligne'
  243. 'Connecting': 'Connexion en cours'
  244. 'Connection re-established': 'Connexion rétablie'
  245. 'Today': 'Aujourdhui'
  246. 'Send': 'Envoyer'
  247. 'Chat closed by %s': 'Chat fermé par %s'
  248. 'Compose your message...': 'Composez votre message...'
  249. 'All colleagues are busy.': 'Tous les collègues sont actuellement occupés.'
  250. 'You are on waiting list position <strong>%s</strong>.': 'Vous êtes actuellement en <strong>%s</strong> position dans la file d\'attente.'
  251. 'Start new conversation': 'Démarrer une nouvelle conversation'
  252. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Si vous ne répondez pas dans les <strong>%s</strong> minutes, votre conversation avec %s va être fermée.'
  253. '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.'
  254. '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!'
  255. 'he':
  256. '<strong>Chat</strong> with us!': '<strong>שוחח</strong>איתנו!'
  257. 'Scroll down to see new messages': 'גלול מטה כדי לראות הודעות חדשות'
  258. 'Online': 'מחובר'
  259. 'Offline': 'מנותק'
  260. 'Connecting': 'מתחבר'
  261. 'Connection re-established': 'החיבור שוחזר'
  262. 'Today': 'היום'
  263. 'Send': 'שלח'
  264. 'Chat closed by %s': 'הצאט נסגר ע"י %s'
  265. 'Compose your message...': 'כתוב את ההודעה שלך ...'
  266. 'All colleagues are busy.': 'כל הנציגים תפוסים'
  267. 'You are on waiting list position <strong>%s</strong>.': 'מיקומך בתור <strong>%s</strong>.'
  268. 'Start new conversation': 'התחל שיחה חדשה'
  269. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'מכיוון שלא הגבת במהלך %s דקות השיחה שלך עם <strong>%s</strong> נסגרה.'
  270. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'מכיוון שלא הגבת במהלך %s הדקות האחרונות השיחה שלך נסגרה.'
  271. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'מצטערים, הזמן לקבלת נציג ארוך מהרגיל. נסה שוב מאוחר יותר או שלח לנו דוא"ל. תודה!'
  272. 'nl':
  273. '<strong>Chat</strong> with us!': '<strong>Chat</strong> met ons!'
  274. 'Scroll down to see new messages': 'Scrol naar beneden om nieuwe berichten te zien'
  275. 'Online': 'Online'
  276. 'Offline': 'Offline'
  277. 'Connecting': 'Verbinden'
  278. 'Connection re-established': 'Verbinding herstelt'
  279. 'Today': 'Vandaag'
  280. 'Send': 'Verzenden'
  281. 'Chat closed by %s': 'Chat gesloten door %s'
  282. 'Compose your message...': 'Typ uw bericht...'
  283. 'All colleagues are busy.': 'Alle medewerkers zijn bezet.'
  284. 'You are on waiting list position <strong>%s</strong>.': 'U bent <strong>%s</strong> in de wachtrij.'
  285. 'Start new conversation': 'Nieuwe conversatie starten'
  286. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met <strong>%s</strong> gesloten.'
  287. '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.'
  288. '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!'
  289. 'it':
  290. '<strong>Chat</strong> with us!': '<strong>Chatta</strong> con noi!'
  291. 'Scroll down to see new messages': 'Scorrere verso il basso per vedere i nuovi messaggi'
  292. 'Online': 'Online'
  293. 'Offline': 'Offline'
  294. 'Connecting': 'Collegamento'
  295. 'Connection re-established': 'Collegamento ristabilito'
  296. 'Today': 'Oggi'
  297. 'Send': 'Invio'
  298. 'Chat closed by %s': 'Conversazione chiusa da %s'
  299. 'Compose your message...': 'Comporre il tuo messaggio...'
  300. 'All colleagues are busy.': 'Tutti i colleghi sono occupati.'
  301. 'You are on waiting list position <strong>%s</strong>.': 'Siete in posizione lista d\' attesa <strong>%s</strong>.'
  302. 'Start new conversation': 'Avviare una nuova conversazione'
  303. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione con <strong>%s</strong> si è chiusa.'
  304. '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.'
  305. '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!'
  306. 'pl':
  307. '<strong>Chat</strong> with us!': '<strong>Czatuj</strong> z nami!'
  308. 'Scroll down to see new messages': 'Przewiń w dół, aby wyświetlić nowe wiadomości'
  309. 'Online': 'Online'
  310. 'Offline': 'Offline'
  311. 'Connecting': 'Łączenie'
  312. 'Connection re-established': 'Ponowne nawiązanie połączenia'
  313. 'Today': 'dzisiejszy'
  314. 'Send': 'Wyślij'
  315. 'Chat closed by %s': 'Czat zamknięty przez %s'
  316. 'Compose your message...': 'Utwórz swoją wiadomość...'
  317. 'All colleagues are busy.': 'Wszyscy koledzy są zajęci.'
  318. 'You are on waiting list position <strong>%s</strong>.': 'Na liście oczekujących znajduje się pozycja <strong>%s</strong>.'
  319. 'Start new conversation': 'Rozpoczęcie nowej konwersacji'
  320. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z <strong>%s</strong> została zamknięta.'
  321. '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.'
  322. '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ę!'
  323. 'zh-cn':
  324. '<strong>Chat</strong> with us!': '发起<strong>即时对话</strong>!'
  325. 'Scroll down to see new messages': '向下滚动以查看新消息'
  326. 'Online': '在线'
  327. 'Offline': '离线'
  328. 'Connecting': '连接中'
  329. 'Connection re-established': '正在重新建立连接'
  330. 'Today': '今天'
  331. 'Send': '发送'
  332. 'Chat closed by %s': 'Chat closed by %s'
  333. 'Compose your message...': '正在输入信息...'
  334. 'All colleagues are busy.': '所有工作人员都在忙碌中.'
  335. 'You are on waiting list position <strong>%s</strong>.': '您目前的等候位置是第 <strong>%s</strong> 位.'
  336. 'Start new conversation': '开始新的会话'
  337. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': '由于您超过 %s 分钟没有回复, 您与 <strong>%s</strong> 的会话已被关闭.'
  338. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': '由于您超过 %s 分钟没有任何回复, 该对话已被关闭.'
  339. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': '非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!'
  340. 'zh-tw':
  341. '<strong>Chat</strong> with us!': '開始<strong>即時對话</strong>!'
  342. 'Scroll down to see new messages': '向下滑動以查看新訊息'
  343. 'Online': '線上'
  344. 'Offline': '离线'
  345. 'Connecting': '連線中'
  346. 'Connection re-established': '正在重新建立連線中'
  347. 'Today': '今天'
  348. 'Send': '發送'
  349. 'Chat closed by %s': 'Chat closed by %s'
  350. 'Compose your message...': '正在輸入訊息...'
  351. 'All colleagues are busy.': '所有服務人員都在忙碌中.'
  352. 'You are on waiting list position <strong>%s</strong>.': '你目前的等候位置是第 <strong>%s</strong> 順位.'
  353. 'Start new conversation': '開始新的對話'
  354. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': '由於你超過 %s 分鐘沒有回應, 你與 <strong>%s</strong> 的對話已被關閉.'
  355. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': '由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.'
  356. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': '非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!'
  357. 'ru':
  358. '<strong>Chat</strong> with us!': 'Напишите нам!'
  359. 'Scroll down to see new messages': 'Прокрутите, чтобы увидеть новые сообщения'
  360. 'Online': 'Онлайн'
  361. 'Offline': 'Оффлайн'
  362. 'Connecting': 'Подключение'
  363. 'Connection re-established': 'Подключение восстановлено'
  364. 'Today': 'Сегодня'
  365. 'Send': 'Отправить'
  366. 'Chat closed by %s': '%s закрыл чат'
  367. 'Compose your message...': 'Напишите сообщение...'
  368. 'All colleagues are busy.': 'Все сотрудники заняты'
  369. 'You are on waiting list position %s.': 'Вы в списке ожидания под номером %s'
  370. 'Start new conversation': 'Начать новую переписку.'
  371. 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.'
  372. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.'
  373. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!'
  374. sessionId: undefined
  375. scrolledToBottom: true
  376. scrollSnapTolerance: 10
  377. richTextFormatKey:
  378. 66: true # b
  379. 73: true # i
  380. 85: true # u
  381. 83: true # s
  382. T: (string, items...) =>
  383. if @options.lang && @options.lang isnt 'en'
  384. if !@translations[@options.lang]
  385. @log.notice "Translation '#{@options.lang}' needed!"
  386. else
  387. translations = @translations[@options.lang]
  388. if !translations[string]
  389. @log.notice "Translation needed for '#{string}'"
  390. string = translations[string] || string
  391. if items
  392. for item in items
  393. string = string.replace(/%s/, item)
  394. string
  395. view: (name) =>
  396. return (options) =>
  397. if !options
  398. options = {}
  399. options.T = @T
  400. options.background = @options.background
  401. options.flat = @options.flat
  402. options.fontSize = @options.fontSize
  403. return window.zammadChatTemplates[name](options)
  404. constructor: (options) ->
  405. super(options)
  406. # jQuery migration
  407. if typeof jQuery != 'undefined' && @options.target instanceof jQuery
  408. @log.notice 'Chat: target option is a jQuery object. jQuery is not a requirement for the chat any more.'
  409. @options.target = @options.target.get(0)
  410. # fullscreen
  411. @isFullscreen = (window.matchMedia and window.matchMedia('(max-width: 768px)').matches)
  412. @scrollRoot = @getScrollRoot()
  413. # check prerequisites
  414. if !window.WebSocket or !sessionStorage
  415. @state = 'unsupported'
  416. @log.notice 'Chat: Browser not supported!'
  417. return
  418. if !@options.chatId
  419. @state = 'unsupported'
  420. @log.error 'Chat: need chatId as option!'
  421. return
  422. # detect language
  423. if !@options.lang
  424. @options.lang = document.documentElement.getAttribute('lang')
  425. if @options.lang
  426. if !@translations[@options.lang]
  427. @log.debug "lang: No #{@options.lang} found, try first two letters"
  428. @options.lang = @options.lang.replace(/-.+?$/, '') # replace "-xx" of xx-xx
  429. @log.debug "lang: #{@options.lang}"
  430. # detect host
  431. @detectHost() if !@options.host
  432. @loadCss()
  433. @io = new Io(@options)
  434. @io.set(
  435. onOpen: @render
  436. onClose: @onWebSocketClose
  437. onMessage: @onWebSocketMessage
  438. onError: @onError
  439. )
  440. @io.connect()
  441. getScrollRoot: ->
  442. return document.scrollingElement if 'scrollingElement' of document
  443. html = document.documentElement
  444. start = parseInt(html.pageYOffset, 10)
  445. html.pageYOffset = start + 1
  446. end = parseInt(html.pageYOffset, 10)
  447. html.pageYOffset = start
  448. return if end > start then html else document.body
  449. render: =>
  450. if !@el || !document.querySelector('.zammad-chat')
  451. @renderBase()
  452. # disable open button
  453. btn = document.querySelector(".#{ @options.buttonClass }")
  454. if btn
  455. btn.classList.add @inactiveClass
  456. @setAgentOnlineState 'online'
  457. @log.debug 'widget rendered'
  458. @startTimeoutObservers()
  459. @idleTimeout.start()
  460. # get current chat status
  461. @sessionId = sessionStorage.getItem('sessionId')
  462. @send 'chat_status_customer',
  463. session_id: @sessionId
  464. url: window.location.href
  465. renderBase: ->
  466. @el.remove() if @el
  467. @options.target.innerHTML += @view('chat')(
  468. title: @options.title,
  469. scrollHint: @options.scrollHint
  470. )
  471. @el = @options.target.querySelector('.zammad-chat')
  472. @input = @el.querySelector('.zammad-chat-input')
  473. @body = @el.querySelector('.zammad-chat-body')
  474. # start bindings
  475. @el.querySelector('.js-chat-open').addEventListener('click', @open)
  476. @el.querySelector('.js-chat-toggle').addEventListener('click', @toggle)
  477. @el.querySelector('.js-chat-status').addEventListener('click', @stopPropagation)
  478. @el.querySelector('.zammad-chat-controls').addEventListener('submit', @onSubmit)
  479. @body.addEventListener('scroll', @detectScrolledtoBottom)
  480. @el.querySelector('.zammad-scroll-hint').addEventListener('click', @onScrollHintClick)
  481. @input.addEventListener('keydown', @onKeydown)
  482. @input.addEventListener('input', @onInput)
  483. @input.addEventListener('paste', @onPaste)
  484. @input.addEventListener('drop', @onDrop)
  485. window.addEventListener('beforeunload', @onLeaveTemporary)
  486. window.addEventListener('hashchange', =>
  487. if @isOpen
  488. if @sessionId
  489. @send 'chat_session_notice',
  490. session_id: @sessionId
  491. message: window.location.href
  492. return
  493. @idleTimeout.start()
  494. )
  495. stopPropagation: (event) ->
  496. event.stopPropagation()
  497. onDrop: (e) =>
  498. e.stopPropagation()
  499. e.preventDefault()
  500. if window.dataTransfer # ie
  501. dataTransfer = window.dataTransfer
  502. else if e.dataTransfer # other browsers
  503. dataTransfer = e.dataTransfer
  504. else
  505. throw 'No clipboardData support'
  506. x = e.clientX
  507. y = e.clientY
  508. file = dataTransfer.files[0]
  509. # look for images
  510. if file.type.match('image.*')
  511. reader = new FileReader()
  512. reader.onload = (e) =>
  513. # Insert the image at the carat
  514. insert = (dataUrl, width) =>
  515. # adapt image if we are on retina devices
  516. if @isRetina()
  517. width = width / 2
  518. result = dataUrl
  519. img = new Image()
  520. img.style.width = '100%'
  521. img.style.maxWidth = width + 'px'
  522. img.src = result
  523. if document.caretPositionFromPoint
  524. pos = document.caretPositionFromPoint(x, y)
  525. range = document.createRange()
  526. range.setStart(pos.offsetNode, pos.offset)
  527. range.collapse()
  528. range.insertNode(img)
  529. else if document.caretRangeFromPoint
  530. range = document.caretRangeFromPoint(x, y)
  531. range.insertNode(img)
  532. else
  533. console.log('could not find carat')
  534. # resize if to big
  535. @resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
  536. reader.readAsDataURL(file)
  537. onPaste: (e) =>
  538. e.stopPropagation()
  539. e.preventDefault()
  540. if e.clipboardData
  541. clipboardData = e.clipboardData
  542. else if window.clipboardData
  543. clipboardData = window.clipboardData
  544. else if e.clipboardData
  545. clipboardData = e.clipboardData
  546. else
  547. throw 'No clipboardData support'
  548. imageInserted = false
  549. if clipboardData && clipboardData.items && clipboardData.items[0]
  550. item = clipboardData.items[0]
  551. if item.kind == 'file' && (item.type == 'image/png' || item.type == 'image/jpeg')
  552. imageFile = item.getAsFile()
  553. reader = new FileReader()
  554. reader.onload = (e) =>
  555. insert = (dataUrl, width) =>
  556. # adapt image if we are on retina devices
  557. if @isRetina()
  558. width = width / 2
  559. img = new Image()
  560. img.style.width = '100%'
  561. img.style.maxWidth = width + 'px'
  562. img.src = dataUrl
  563. document.execCommand('insertHTML', false, img)
  564. # resize if to big
  565. @resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
  566. reader.readAsDataURL(imageFile)
  567. imageInserted = true
  568. return if imageInserted
  569. # check existing + paste text for limit
  570. text = undefined
  571. docType = undefined
  572. try
  573. text = clipboardData.getData('text/html')
  574. docType = 'html'
  575. if !text || text.length is 0
  576. docType = 'text'
  577. text = clipboardData.getData('text/plain')
  578. if !text || text.length is 0
  579. docType = 'text2'
  580. text = clipboardData.getData('text')
  581. catch e
  582. console.log('Sorry, can\'t insert markup because browser is not supporting it.')
  583. docType = 'text3'
  584. text = clipboardData.getData('text')
  585. if docType is 'text' || docType is 'text2' || docType is 'text3'
  586. text = '<div>' + text.replace(/\n/g, '</div><div>') + '</div>'
  587. text = text.replace(/<div><\/div>/g, '<div><br></div>')
  588. console.log('p', docType, text)
  589. if docType is 'html'
  590. html = document.createElement('div')
  591. html.innerHTML = text
  592. match = false
  593. htmlTmp = text
  594. regex = new RegExp('<(/w|w)\:[A-Za-z]')
  595. if htmlTmp.match(regex)
  596. match = true
  597. htmlTmp = htmlTmp.replace(regex, '')
  598. regex = new RegExp('<(/o|o)\:[A-Za-z]')
  599. if htmlTmp.match(regex)
  600. match = true
  601. htmlTmp = htmlTmp.replace(regex, '')
  602. if match
  603. html = @wordFilter(html)
  604. #html
  605. for node in html.childNodes
  606. if node.nodeType == 8
  607. node.remove()
  608. # remove tags, keep content
  609. for node in html.querySelectorAll('a, font, small, time, form, label')
  610. node.outerHTML = node.innerHTML
  611. # replace tags with generic div
  612. # New type of the tag
  613. replacementTag = 'div';
  614. # Replace all x tags with the type of replacementTag
  615. for node in html.querySelectorAll('textarea')
  616. outer = node.outerHTML
  617. # Replace opening tag
  618. regex = new RegExp('<' + node.tagName, 'i')
  619. newTag = outer.replace(regex, '<' + replacementTag)
  620. # Replace closing tag
  621. regex = new RegExp('</' + node.tagName, 'i')
  622. newTag = newTag.replace(regex, '</' + replacementTag)
  623. node.outerHTML = newTag
  624. # remove tags & content
  625. for node in html.querySelectorAll('font, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset')
  626. node.remove()
  627. @removeAttributes(html)
  628. text = html.innerHTML
  629. # as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower)
  630. if docType is 'text3'
  631. @pasteHtmlAtCaret(text)
  632. else
  633. document.execCommand('insertHTML', false, text)
  634. true
  635. onKeydown: (e) =>
  636. # check for enter
  637. if not e.shiftKey and e.keyCode is 13
  638. e.preventDefault()
  639. @sendMessage()
  640. richtTextControl = false
  641. if !e.altKey && !e.ctrlKey && e.metaKey
  642. richtTextControl = true
  643. else if !e.altKey && e.ctrlKey && !e.metaKey
  644. richtTextControl = true
  645. if richtTextControl && @richTextFormatKey[ e.keyCode ]
  646. e.preventDefault()
  647. if e.keyCode is 66
  648. document.execCommand('bold')
  649. return true
  650. if e.keyCode is 73
  651. document.execCommand('italic')
  652. return true
  653. if e.keyCode is 85
  654. document.execCommand('underline')
  655. return true
  656. if e.keyCode is 83
  657. document.execCommand('strikeThrough')
  658. return true
  659. send: (event, data = {}) =>
  660. data.chat_id = @options.chatId
  661. @io.send(event, data)
  662. onWebSocketMessage: (pipes) =>
  663. for pipe in pipes
  664. @log.debug 'ws:onmessage', pipe
  665. switch pipe.event
  666. when 'chat_error'
  667. @log.notice pipe.data
  668. if pipe.data && pipe.data.state is 'chat_disabled'
  669. @destroy(remove: true)
  670. when 'chat_session_message'
  671. return if pipe.data.self_written
  672. @receiveMessage pipe.data
  673. when 'chat_session_typing'
  674. return if pipe.data.self_written
  675. @onAgentTypingStart()
  676. when 'chat_session_start'
  677. @onConnectionEstablished pipe.data
  678. when 'chat_session_queue'
  679. @onQueueScreen pipe.data
  680. when 'chat_session_closed'
  681. @onSessionClosed pipe.data
  682. when 'chat_session_left'
  683. @onSessionClosed pipe.data
  684. when 'chat_status_customer'
  685. switch pipe.data.state
  686. when 'online'
  687. @sessionId = undefined
  688. if !@options.cssAutoload || @cssLoaded
  689. @onReady()
  690. else
  691. @socketReady = true
  692. when 'offline'
  693. @onError 'Zammad Chat: No agent online'
  694. when 'chat_disabled'
  695. @onError 'Zammad Chat: Chat is disabled'
  696. when 'no_seats_available'
  697. @onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}"
  698. when 'reconnect'
  699. @onReopenSession pipe.data
  700. onReady: ->
  701. @log.debug 'widget ready for use'
  702. btn = document.querySelector(".#{ @options.buttonClass }")
  703. if btn
  704. btn.addEventListener('click', @open)
  705. btn.classList.remove(@inactiveClass)
  706. @options.onReady?()
  707. if @options.show
  708. @show()
  709. onError: (message) =>
  710. @log.debug message
  711. @addStatus(message)
  712. btn = document.querySelector(".#{ @options.buttonClass }")
  713. if btn
  714. btn.classList.add('zammad-chat-is-hidden')
  715. if @isOpen
  716. @disableInput()
  717. @destroy(remove: false)
  718. else
  719. @destroy(remove: true)
  720. @options.onError?(message)
  721. onReopenSession: (data) =>
  722. @log.debug 'old messages', data.session
  723. @inactiveTimeout.start()
  724. unfinishedMessage = sessionStorage.getItem 'unfinished_message'
  725. # rerender chat history
  726. if data.agent
  727. @onConnectionEstablished(data)
  728. for message in data.session
  729. @renderMessage
  730. message: message.content
  731. id: message.id
  732. from: if message.created_by_id then 'agent' else 'customer'
  733. if unfinishedMessage
  734. @input.innerHTML = unfinishedMessage
  735. # show wait list
  736. if data.position
  737. @onQueue data
  738. @show()
  739. @open()
  740. @scrollToBottom()
  741. if unfinishedMessage
  742. @input.focus()
  743. onInput: =>
  744. # remove unread-state from messages
  745. for message in @el.querySelectorAll('.zammad-chat-message--unread')
  746. node.classList.remove 'zammad-chat-message--unread'
  747. sessionStorage.setItem 'unfinished_message', @input.innerHTML
  748. @onTyping()
  749. onTyping: ->
  750. # send typing start event only every 1.5 seconds
  751. return if @isTyping && @isTyping > new Date(new Date().getTime() - 1500)
  752. @isTyping = new Date()
  753. @send 'chat_session_typing',
  754. session_id: @sessionId
  755. @inactiveTimeout.start()
  756. onSubmit: (event) =>
  757. event.preventDefault()
  758. @sendMessage()
  759. sendMessage: ->
  760. message = @input.innerHTML
  761. return if !message
  762. @inactiveTimeout.start()
  763. sessionStorage.removeItem 'unfinished_message'
  764. messageElement = @view('message')
  765. message: message
  766. from: 'customer'
  767. id: @_messageCount++
  768. unreadClass: ''
  769. @maybeAddTimestamp()
  770. # add message before message typing loader
  771. if @el.querySelector('.zammad-chat-message--typing')
  772. @lastAddedType = 'typing-placeholder'
  773. @el.querySelector('.zammad-chat-message--typing').insertAdjacentHTML('beforebegin', messageElement)
  774. else
  775. @lastAddedType = 'message--customer'
  776. @body.insertAdjacentHTML('beforeend', messageElement)
  777. @input.innerHTML = ''
  778. @scrollToBottom()
  779. # send message event
  780. @send 'chat_session_message',
  781. content: message
  782. id: @_messageCount
  783. session_id: @sessionId
  784. receiveMessage: (data) =>
  785. @inactiveTimeout.start()
  786. # hide writing indicator
  787. @onAgentTypingEnd()
  788. @maybeAddTimestamp()
  789. @renderMessage
  790. message: data.message.content
  791. id: data.id
  792. from: 'agent'
  793. @scrollToBottom showHint: true
  794. renderMessage: (data) =>
  795. @lastAddedType = "message--#{ data.from }"
  796. data.unreadClass = if document.hidden then ' zammad-chat-message--unread' else ''
  797. @body.insertAdjacentHTML('beforeend', @view('message')(data))
  798. open: =>
  799. if @isOpen
  800. @log.debug 'widget already open, block'
  801. return
  802. @isOpen = true
  803. @log.debug 'open widget'
  804. @show()
  805. if !@sessionId
  806. @showLoader()
  807. @el.classList.add 'zammad-chat-is-open'
  808. remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight
  809. @el.style.transform = "translateY(#{remainerHeight}px)"
  810. # force redraw
  811. @el.clientHeight
  812. if !@sessionId
  813. @el.addEventListener 'transitionend', @onOpenAnimationEnd
  814. @el.classList.add 'zammad-chat--animate'
  815. # force redraw
  816. @el.clientHeight
  817. # start animation
  818. @el.style.transform = ''
  819. @send('chat_session_init'
  820. url: window.location.href
  821. )
  822. else
  823. @el.style.transform = ''
  824. @onOpenAnimationEnd()
  825. onOpenAnimationEnd: =>
  826. @el.removeEventListener 'transitionend', @onOpenAnimationEnd
  827. @el.classList.remove 'zammad-chat--animate'
  828. @idleTimeout.stop()
  829. if @isFullscreen
  830. @disableScrollOnRoot()
  831. @options.onOpenAnimationEnd?()
  832. sessionClose: =>
  833. # send close
  834. @send 'chat_session_close',
  835. session_id: @sessionId
  836. # stop timer
  837. @inactiveTimeout.stop()
  838. @waitingListTimeout.stop()
  839. # delete input store
  840. sessionStorage.removeItem 'unfinished_message'
  841. # stop delay of initial queue position
  842. if @onInitialQueueDelayId
  843. clearTimeout(@onInitialQueueDelayId)
  844. @setSessionId undefined
  845. toggle: (event) =>
  846. if @isOpen
  847. @close(event)
  848. else
  849. @open(event)
  850. close: (event) =>
  851. if !@isOpen
  852. @log.debug 'can\'t close widget, it\'s not open'
  853. return
  854. if @initDelayId
  855. clearTimeout(@initDelayId)
  856. if !@sessionId
  857. @log.debug 'can\'t close widget without sessionId'
  858. return
  859. @log.debug 'close widget'
  860. event.stopPropagation() if event
  861. @sessionClose()
  862. if @isFullscreen
  863. @enableScrollOnRoot()
  864. # close window
  865. remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight
  866. @el.addEventListener 'transitionend', @onCloseAnimationEnd
  867. @el.classList.add 'zammad-chat--animate'
  868. # force redraw
  869. document.offsetHeight
  870. # animate out
  871. @el.style.transform = "translateY(#{remainerHeight}px)"
  872. onCloseAnimationEnd: =>
  873. @el.removeEventListener 'transitionend', @onCloseAnimationEnd
  874. @el.classList.remove 'zammad-chat-is-open', 'zammad-chat--animate'
  875. @el.style.transform = ''
  876. @showLoader()
  877. @el.querySelector('.zammad-chat-welcome').classList.remove('zammad-chat-is-hidden')
  878. @el.querySelector('.zammad-chat-agent').classList.add('zammad-chat-is-hidden')
  879. @el.querySelector('.zammad-chat-agent-status').classList.add('zammad-chat-is-hidden')
  880. @isOpen = false
  881. @options.onCloseAnimationEnd?()
  882. @io.reconnect()
  883. onWebSocketClose: =>
  884. return if @isOpen
  885. if @el
  886. @el.classList.remove('zammad-chat-is-shown')
  887. @el.classList.remove('zammad-chat-is-loaded')
  888. show: ->
  889. return if @state is 'offline'
  890. @el.classList.add('zammad-chat-is-loaded')
  891. @el.classList.add('zammad-chat-is-shown')
  892. disableInput: ->
  893. @input.disabled = true
  894. @el.querySelector('.zammad-chat-send').disabled = true
  895. enableInput: ->
  896. @input.disabled = false
  897. @el.querySelector('.zammad-chat-send').disabled = false
  898. hideModal: ->
  899. @el.querySelector('.zammad-chat-modal').innerHTML = ''
  900. onQueueScreen: (data) =>
  901. @setSessionId data.session_id
  902. # delay initial queue position, show connecting first
  903. show = =>
  904. @onQueue data
  905. @waitingListTimeout.start()
  906. if @initialQueueDelay && !@onInitialQueueDelayId
  907. @onInitialQueueDelayId = setTimeout(show, @initialQueueDelay)
  908. return
  909. # stop delay of initial queue position
  910. if @onInitialQueueDelayId
  911. clearTimeout(@onInitialQueueDelayId)
  912. # show queue position
  913. show()
  914. onQueue: (data) =>
  915. @log.notice 'onQueue', data.position
  916. @inQueue = true
  917. @el.querySelector('.zammad-chat-modal').innerHTML = @view('waiting')
  918. position: data.position
  919. onAgentTypingStart: =>
  920. if @stopTypingId
  921. clearTimeout(@stopTypingId)
  922. @stopTypingId = setTimeout(@onAgentTypingEnd, 3000)
  923. # never display two typing indicators
  924. return if @el.querySelector('.zammad-chat-message--typing')
  925. @maybeAddTimestamp()
  926. @body.insertAdjacentHTML('beforeend', @view('typingIndicator')())
  927. # only if typing indicator is shown
  928. return if !@isVisible(@el.querySelector('.zammad-chat-message--typing'), true)
  929. @scrollToBottom()
  930. onAgentTypingEnd: =>
  931. @el.querySelector('.zammad-chat-message--typing').remove() if @el.querySelector('.zammad-chat-message--typing')
  932. onLeaveTemporary: =>
  933. return if !@sessionId
  934. @send 'chat_session_leave_temporary',
  935. session_id: @sessionId
  936. maybeAddTimestamp: ->
  937. timestamp = Date.now()
  938. if !@lastTimestamp or (timestamp - @lastTimestamp) > @showTimeEveryXMinutes * 60000
  939. label = @T('Today')
  940. time = new Date().toTimeString().substr 0,5
  941. if @lastAddedType is 'timestamp'
  942. # update last time
  943. @updateLastTimestamp label, time
  944. @lastTimestamp = timestamp
  945. else
  946. # add new timestamp
  947. @body.insertAdjacentHTML 'beforeend', @view('timestamp')
  948. label: label
  949. time: time
  950. @lastTimestamp = timestamp
  951. @lastAddedType = 'timestamp'
  952. @scrollToBottom()
  953. updateLastTimestamp: (label, time) ->
  954. return if !@el
  955. timestamps = @el.querySelectorAll('.zammad-chat-body .zammad-chat-timestamp')
  956. return if !timestamps
  957. timestamps[timestamps.length - 1].outerHTML = @view('timestamp')
  958. label: label
  959. time: time
  960. addStatus: (status) ->
  961. return if !@el
  962. @maybeAddTimestamp()
  963. @body.insertAdjacentHTML 'beforeend', @view('status')
  964. status: status
  965. @scrollToBottom()
  966. detectScrolledtoBottom: =>
  967. scrollBottom = @body.scrollTop + @body.offsetHeight
  968. @scrolledToBottom = Math.abs(scrollBottom - @body.scrollHeight) <= @scrollSnapTolerance
  969. @el.querySelector('.zammad-scroll-hint').classList.add('is-hidden') if @scrolledToBottom
  970. showScrollHint: ->
  971. @el.querySelector('.zammad-scroll-hint').classList.remove('is-hidden')
  972. # compensate scroll
  973. @body.scrollTop = @body.scrollTop + @el.querySelector('.zammad-scroll-hint').offsetHeight
  974. onScrollHintClick: =>
  975. # animate scroll
  976. @body.scrollTo
  977. top: @body.scrollHeight
  978. behavior: 'smooth'
  979. scrollToBottom: ({ showHint } = { showHint: false }) ->
  980. if @scrolledToBottom
  981. @body.scrollTop = @body.scrollHeight
  982. else if showHint
  983. @showScrollHint()
  984. destroy: (params = {}) =>
  985. @log.debug 'destroy widget', params
  986. @setAgentOnlineState 'offline'
  987. if params.remove && @el
  988. @el.remove()
  989. # stop all timer
  990. if @waitingListTimeout
  991. @waitingListTimeout.stop()
  992. if @inactiveTimeout
  993. @inactiveTimeout.stop()
  994. if @idleTimeout
  995. @idleTimeout.stop()
  996. # stop ws connection
  997. @io.close()
  998. reconnect: =>
  999. # set status to connecting
  1000. @log.notice 'reconnecting'
  1001. @disableInput()
  1002. @lastAddedType = 'status'
  1003. @setAgentOnlineState 'connecting'
  1004. @addStatus @T('Connection lost')
  1005. onConnectionReestablished: =>
  1006. # set status back to online
  1007. @lastAddedType = 'status'
  1008. @setAgentOnlineState 'online'
  1009. @addStatus @T('Connection re-established')
  1010. @options.onConnectionReestablished?()
  1011. onSessionClosed: (data) ->
  1012. @addStatus @T('Chat closed by %s', data.realname)
  1013. @disableInput()
  1014. @setAgentOnlineState 'offline'
  1015. @inactiveTimeout.stop()
  1016. @options.onSessionClosed?(data)
  1017. setSessionId: (id) =>
  1018. @sessionId = id
  1019. if id is undefined
  1020. sessionStorage.removeItem 'sessionId'
  1021. else
  1022. sessionStorage.setItem 'sessionId', id
  1023. onConnectionEstablished: (data) =>
  1024. # stop delay of initial queue position
  1025. if @onInitialQueueDelayId
  1026. clearTimeout @onInitialQueueDelayId
  1027. @inQueue = false
  1028. if data.agent
  1029. @agent = data.agent
  1030. if data.session_id
  1031. @setSessionId data.session_id
  1032. # empty old messages
  1033. @body.innerHTML = ''
  1034. @el.querySelector('.zammad-chat-agent').innerHTML = @view('agent')
  1035. agent: @agent
  1036. @enableInput()
  1037. @hideModal()
  1038. @el.querySelector('.zammad-chat-welcome').classList.add('zammad-chat-is-hidden')
  1039. @el.querySelector('.zammad-chat-agent').classList.remove('zammad-chat-is-hidden')
  1040. @el.querySelector('.zammad-chat-agent-status').classList.remove('zammad-chat-is-hidden')
  1041. @input.focus() if not @isFullscreen
  1042. @setAgentOnlineState 'online'
  1043. @waitingListTimeout.stop()
  1044. @idleTimeout.stop()
  1045. @inactiveTimeout.start()
  1046. @options.onConnectionEstablished?(data)
  1047. showCustomerTimeout: ->
  1048. @el.querySelector('.zammad-chat-modal').innerHTML = @view('customer_timeout')
  1049. agent: @agent.name
  1050. delay: @options.inactiveTimeout
  1051. @el.querySelector('.js-restart').addEventListener 'click', -> location.reload()
  1052. @sessionClose()
  1053. showWaitingListTimeout: ->
  1054. @el.querySelector('.zammad-chat-modal').innerHTML = @view('waiting_list_timeout')
  1055. delay: @options.watingListTimeout
  1056. @el.querySelector('.js-restart').addEventListener 'click', -> location.reload()
  1057. @sessionClose()
  1058. showLoader: ->
  1059. @el.querySelector('.zammad-chat-modal').innerHTML = @view('loader')()
  1060. setAgentOnlineState: (state) =>
  1061. @state = state
  1062. return if !@el
  1063. capitalizedState = state.charAt(0).toUpperCase() + state.slice(1)
  1064. @el.querySelector('.zammad-chat-agent-status').dataset.status = state
  1065. @el.querySelector('.zammad-chat-agent-status').textContent = @T(capitalizedState)
  1066. detectHost: ->
  1067. protocol = 'ws://'
  1068. if scriptProtocol is 'https'
  1069. protocol = 'wss://'
  1070. @options.host = "#{ protocol }#{ scriptHost }/ws"
  1071. loadCss: ->
  1072. return if !@options.cssAutoload
  1073. url = @options.cssUrl
  1074. if !url
  1075. url = @options.host
  1076. .replace(/^wss/i, 'https')
  1077. .replace(/^ws/i, 'http')
  1078. .replace(/\/ws/i, '')
  1079. url += '/assets/chat/chat.css'
  1080. @log.debug "load css from '#{url}'"
  1081. styles = "@import url('#{url}');"
  1082. newSS = document.createElement('link')
  1083. newSS.onload = @onCssLoaded
  1084. newSS.rel = 'stylesheet'
  1085. newSS.href = 'data:text/css,' + escape(styles)
  1086. document.getElementsByTagName('head')[0].appendChild(newSS)
  1087. onCssLoaded: =>
  1088. @cssLoaded = true
  1089. if @socketReady
  1090. @onReady()
  1091. @options.onCssLoaded?()
  1092. startTimeoutObservers: =>
  1093. @idleTimeout = new Timeout(
  1094. logPrefix: 'idleTimeout'
  1095. debug: @options.debug
  1096. timeout: @options.idleTimeout
  1097. timeoutIntervallCheck: @options.idleTimeoutIntervallCheck
  1098. callback: =>
  1099. @log.debug 'Idle timeout reached, hide widget', new Date
  1100. @destroy(remove: true)
  1101. )
  1102. @inactiveTimeout = new Timeout(
  1103. logPrefix: 'inactiveTimeout'
  1104. debug: @options.debug
  1105. timeout: @options.inactiveTimeout
  1106. timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck
  1107. callback: =>
  1108. @log.debug 'Inactive timeout reached, show timeout screen.', new Date
  1109. @showCustomerTimeout()
  1110. @destroy(remove: false)
  1111. )
  1112. @waitingListTimeout = new Timeout(
  1113. logPrefix: 'waitingListTimeout'
  1114. debug: @options.debug
  1115. timeout: @options.waitingListTimeout
  1116. timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck
  1117. callback: =>
  1118. @log.debug 'Waiting list timeout reached, show timeout screen.', new Date
  1119. @showWaitingListTimeout()
  1120. @destroy(remove: false)
  1121. )
  1122. disableScrollOnRoot: ->
  1123. @rootScrollOffset = @scrollRoot.scrollTop
  1124. @scrollRoot.style.overflow = 'hidden'
  1125. @scrollRoot.style.position = 'fixed'
  1126. enableScrollOnRoot: ->
  1127. @scrollRoot.scrollTop = @rootScrollOffset
  1128. @scrollRoot.style.overflow = ''
  1129. @scrollRoot.style.position = ''
  1130. # based on https://github.com/customd/jquery-visible/blob/master/jquery.visible.js
  1131. # to have not dependency, port to coffeescript
  1132. isVisible: (el, partial, hidden, direction) ->
  1133. return if el.length < 1
  1134. vpWidth = window.innerWidth
  1135. vpHeight = window.innerHeight
  1136. direction = if direction then direction else 'both'
  1137. clientSize = if hidden is true then t.offsetWidth * t.offsetHeight else true
  1138. rec = el.getBoundingClientRect()
  1139. tViz = rec.top >= 0 && rec.top < vpHeight
  1140. bViz = rec.bottom > 0 && rec.bottom <= vpHeight
  1141. lViz = rec.left >= 0 && rec.left < vpWidth
  1142. rViz = rec.right > 0 && rec.right <= vpWidth
  1143. vVisible = if partial then tViz || bViz else tViz && bViz
  1144. hVisible = if partial then lViz || rViz else lViz && rViz
  1145. if direction is 'both'
  1146. return clientSize && vVisible && hVisible
  1147. else if direction is 'vertical'
  1148. return clientSize && vVisible
  1149. else if direction is 'horizontal'
  1150. return clientSize && hVisible
  1151. isRetina: ->
  1152. if window.matchMedia
  1153. 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)')
  1154. return (mq && mq.matches || (window.devicePixelRatio > 1))
  1155. false
  1156. resizeImage: (dataURL, x = 'auto', y = 'auto', sizeFactor = 1, type, quallity, callback, force = true) ->
  1157. # load image from data url
  1158. imageObject = new Image()
  1159. imageObject.onload = ->
  1160. imageWidth = imageObject.width
  1161. imageHeight = imageObject.height
  1162. console.log('ImageService', 'current size', imageWidth, imageHeight)
  1163. if y is 'auto' && x is 'auto'
  1164. x = imageWidth
  1165. y = imageHeight
  1166. # get auto dimensions
  1167. if y is 'auto'
  1168. factor = imageWidth / x
  1169. y = imageHeight / factor
  1170. if x is 'auto'
  1171. factor = imageWidth / y
  1172. x = imageHeight / factor
  1173. # check if resize is needed
  1174. resize = false
  1175. if x < imageWidth || y < imageHeight
  1176. resize = true
  1177. x = x * sizeFactor
  1178. y = y * sizeFactor
  1179. else
  1180. x = imageWidth
  1181. y = imageHeight
  1182. # create canvas and set dimensions
  1183. canvas = document.createElement('canvas')
  1184. canvas.width = x
  1185. canvas.height = y
  1186. # draw image on canvas and set image dimensions
  1187. context = canvas.getContext('2d')
  1188. context.drawImage(imageObject, 0, 0, x, y)
  1189. # set quallity based on image size
  1190. if quallity == 'auto'
  1191. if x < 200 && y < 200
  1192. quallity = 1
  1193. else if x < 400 && y < 400
  1194. quallity = 0.9
  1195. else if x < 600 && y < 600
  1196. quallity = 0.8
  1197. else if x < 900 && y < 900
  1198. quallity = 0.7
  1199. else
  1200. quallity = 0.6
  1201. # execute callback with resized image
  1202. newDataUrl = canvas.toDataURL(type, quallity)
  1203. if resize
  1204. console.log('ImageService', 'resize', x/sizeFactor, y/sizeFactor, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
  1205. callback(newDataUrl, x/sizeFactor, y/sizeFactor, true)
  1206. return
  1207. console.log('ImageService', 'no resize', x, y, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
  1208. callback(newDataUrl, x, y, false)
  1209. # load image from data url
  1210. imageObject.src = dataURL
  1211. # taken from https://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
  1212. pasteHtmlAtCaret: (html) ->
  1213. sel = undefined
  1214. range = undefined
  1215. if window.getSelection
  1216. sel = window.getSelection()
  1217. if sel.getRangeAt && sel.rangeCount
  1218. range = sel.getRangeAt(0)
  1219. range.deleteContents()
  1220. el = document.createElement('div')
  1221. el.innerHTML = html
  1222. frag = document.createDocumentFragment(node, lastNode)
  1223. while node = el.firstChild
  1224. lastNode = frag.appendChild(node)
  1225. range.insertNode(frag)
  1226. if lastNode
  1227. range = range.cloneRange()
  1228. range.setStartAfter(lastNode)
  1229. range.collapse(true)
  1230. sel.removeAllRanges()
  1231. sel.addRange(range)
  1232. else if document.selection && document.selection.type != 'Control'
  1233. document.selection.createRange().pasteHTML(html)
  1234. # (C) sbrin - https://github.com/sbrin
  1235. # https://gist.github.com/sbrin/6801034
  1236. wordFilter: (editor) ->
  1237. content = editor.html()
  1238. # Word comments like conditional comments etc
  1239. content = content.replace(/<!--[\s\S]+?-->/gi, '')
  1240. # Remove comments, scripts (e.g., msoShowComment), XML tag, VML content,
  1241. # MS Office namespaced tags, and a few other tags
  1242. content = content.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, '')
  1243. # Convert <s> into <strike> for line-though
  1244. content = content.replace(/<(\/?)s>/gi, '<$1strike>')
  1245. # Replace nbsp entites to char since it's easier to handle
  1246. # content = content.replace(/&nbsp;/gi, "\u00a0")
  1247. content = content.replace(/&nbsp;/gi, ' ')
  1248. # Convert <span style="mso-spacerun:yes">___</span> to string of alternating
  1249. # breaking/non-breaking spaces of same length
  1250. #content = content.replace(/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi, (str, spaces) ->
  1251. # return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : ''
  1252. #)
  1253. editor.innerHTML = content
  1254. # Parse out list indent level for lists
  1255. for p in editor.querySelectorAll('p')
  1256. str = p.getAttribute('style')
  1257. matches = /mso-list:\w+ \w+([0-9]+)/.exec(str)
  1258. if matches
  1259. p.dataset._listLevel = parseInt(matches[1], 10)
  1260. # Parse Lists
  1261. last_level = 0
  1262. pnt = null
  1263. for p in editor.querySelectorAll('p')
  1264. cur_level = p.dataset._listLevel
  1265. if cur_level != undefined
  1266. txt = p.textContent
  1267. list_tag = '<ul></ul>'
  1268. if (/^\s*\w+\./.test(txt))
  1269. matches = /([0-9])\./.exec(txt)
  1270. if matches
  1271. start = parseInt(matches[1], 10)
  1272. list_tag = start>1 ? '<ol start="' + start + '"></ol>' : '<ol></ol>'
  1273. else
  1274. list_tag = '<ol></ol>'
  1275. if cur_level > last_level
  1276. if last_level == 0
  1277. p.insertAdjacentHTML 'beforebegin', list_tag
  1278. pnt = p.previousElementSibling
  1279. else
  1280. pnt.insertAdjacentHTML 'beforeend', list_tag
  1281. if cur_level < last_level
  1282. for i in [i..last_level-cur_level]
  1283. pnt = pnt.parentNode
  1284. p.querySelector('span:first').remove() if p.querySelector('span:first')
  1285. pnt.insertAdjacentHTML 'beforeend', '<li>' + p.innerHTML + '</li>'
  1286. p.remove()
  1287. last_level = cur_level
  1288. else
  1289. last_level = 0
  1290. el.removeAttribute('style') for el in editor.querySelectorAll('[style]')
  1291. el.removeAttribute('align') for el in editor.querySelectorAll('[align]')
  1292. el.outerHTML = el.innerHTML for el in editor.querySelectorAll('span')
  1293. el.remove() for el in editor.querySelectorAll('span:empty')
  1294. el.removeAttribute('class') for el in editor.querySelectorAll("[class^='Mso']")
  1295. el.remove() for el in editor.querySelectorAll('p:empty')
  1296. editor
  1297. removeAttribute: (element) ->
  1298. return if !element
  1299. for att in element.attributes
  1300. element.removeAttribute(att.name)
  1301. removeAttributes: (html) =>
  1302. for node in html.querySelectorAll('*')
  1303. @removeAttribute node
  1304. html
  1305. window.ZammadChat = ZammadChat