chat-no-jquery.coffee 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569
  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. inputDisabled: false
  166. inputTimeout: null
  167. isTyping: false
  168. state: 'offline'
  169. initialQueueDelay: 10000
  170. translations:
  171. # ZAMMAD_TRANSLATIONS_START
  172. 'de':
  173. '<strong>Chat</strong> with us!': '<strong>Chatte</strong> mit uns!'
  174. 'All colleagues are busy.': 'Alle Kollegen sind beschäftigt.'
  175. 'Chat closed by %s': 'Chat von %s geschlossen'
  176. 'Compose your message…': 'Verfassen Sie Ihre Nachricht…'
  177. 'Connecting': 'Verbinde'
  178. 'Connection lost': 'Verbindung verloren'
  179. 'Connection re-established': 'Verbindung wieder aufgebaut'
  180. 'Offline': 'Offline'
  181. 'Online': 'Online'
  182. 'Scroll down to see new messages': 'Nach unten scrollen um neue Nachrichten zu sehen'
  183. 'Send': 'Senden'
  184. '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.'
  185. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Da Sie innerhalb der letzten %s Minuten nicht reagiert haben, wurde Ihre Unterhaltung mit <strong>%s</strong> geschlossen.'
  186. 'Start new conversation': 'Neue Unterhaltung starten'
  187. 'Today': 'Heute'
  188. '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!'
  189. 'You are on waiting list position <strong>%s</strong>.': 'Sie sind in der Warteliste auf Position <strong>%s</strong>.'
  190. 'es':
  191. '<strong>Chat</strong> with us!': '<strong>Chatee</strong> con nosotros!'
  192. 'All colleagues are busy.': 'Todos los colegas están ocupados.'
  193. 'Chat closed by %s': 'Chat cerrado por %s'
  194. 'Compose your message…': 'Escribe tu mensaje…'
  195. 'Connecting': 'Conectando'
  196. 'Connection lost': 'Conexión perdida'
  197. 'Connection re-established': 'Conexión reestablecida'
  198. 'Offline': 'Desconectado'
  199. 'Online': 'En línea'
  200. 'Scroll down to see new messages': 'Haga scroll hacia abajo para ver nuevos mensajes'
  201. 'Send': 'Enviar'
  202. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  203. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  204. 'Start new conversation': 'Iniciar nueva conversación'
  205. 'Today': 'Hoy'
  206. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': ''
  207. 'You are on waiting list position <strong>%s</strong>.': 'Usted está en la posición <strong>%s</strong> de la lista de espera.'
  208. 'fr':
  209. '<strong>Chat</strong> with us!': '<strong>Chattez</strong> avec nous!'
  210. 'All colleagues are busy.': 'Tout les agents sont occupés.'
  211. 'Chat closed by %s': 'Chat fermé par %s'
  212. 'Compose your message…': 'Ecrivez votre message…'
  213. 'Connecting': 'Connexion'
  214. 'Connection lost': 'Connexion perdue'
  215. 'Connection re-established': 'Connexion ré-établie'
  216. 'Offline': 'Hors-ligne'
  217. 'Online': 'En ligne'
  218. 'Scroll down to see new messages': 'Défiler vers le bas pour voir les nouveaux messages'
  219. 'Send': 'Envoyer'
  220. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  221. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  222. 'Start new conversation': 'Démarrer une nouvelle conversation'
  223. 'Today': 'Aujourd\'hui'
  224. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': ''
  225. 'You are on waiting list position <strong>%s</strong>.': 'Vous êtes actuellement en position <strong>%s</strong> dans la file d\'attente.'
  226. 'hr':
  227. '<strong>Chat</strong> with us!': '<strong>Čavrljajte</strong> sa nama!'
  228. 'All colleagues are busy.': 'Svi kolege su zauzeti.'
  229. 'Chat closed by %s': '%s zatvara chat'
  230. 'Compose your message…': 'Sastavite poruku…'
  231. 'Connecting': 'Povezivanje'
  232. 'Connection lost': 'Veza prekinuta'
  233. 'Connection re-established': 'Veza je ponovno uspostavljena'
  234. 'Offline': 'Odsutan'
  235. 'Online': 'Dostupan(a)'
  236. 'Scroll down to see new messages': 'Pomaknite se prema dolje da biste vidjeli nove poruke'
  237. 'Send': 'Šalji'
  238. '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.'
  239. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Budući da niste odgovorili u posljednjih %s minuta, Vaš je razgovor s <strong>%</strong>s zatvoren.'
  240. 'Start new conversation': 'Započni novi razgovor'
  241. 'Today': 'Danas'
  242. '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!'
  243. 'You are on waiting list position <strong>%s</strong>.': 'Nalazite se u redu čekanja na poziciji <strong>%s</strong>.'
  244. 'it':
  245. '<strong>Chat</strong> with us!': '<strong>Chatta</strong> con noi!'
  246. 'All colleagues are busy.': 'Tutti i colleghi sono occupati.'
  247. 'Chat closed by %s': 'Chat chiusa da %s'
  248. 'Compose your message…': 'Scrivi il tuo messaggio…'
  249. 'Connecting': 'Connessione in corso'
  250. 'Connection lost': 'Connessione persa'
  251. 'Connection re-established': 'Connessione ristabilita'
  252. 'Offline': 'Offline'
  253. 'Online': 'Online'
  254. 'Scroll down to see new messages': 'Scorri verso il basso per vedere i nuovi messaggi'
  255. 'Send': 'Invia'
  256. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  257. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  258. 'Start new conversation': 'Avvia una nuova chat'
  259. 'Today': 'Oggi'
  260. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': ''
  261. 'You are on waiting list position <strong>%s</strong>.': 'Sei alla posizione <strong>%s</strong> della lista di attesa.'
  262. 'nl':
  263. '<strong>Chat</strong> with us!': '<strong>Chat</strong> met ons!'
  264. 'All colleagues are busy.': 'Alle collega\'s zijn bezet.'
  265. 'Chat closed by %s': 'Chat gesloten door %s'
  266. 'Compose your message…': 'Stel je bericht op…'
  267. 'Connecting': 'Verbinden'
  268. 'Connection lost': 'Verbinding verbroken'
  269. 'Connection re-established': 'Verbinding hersteld'
  270. 'Offline': 'Offline'
  271. 'Online': 'Online'
  272. 'Scroll down to see new messages': 'Scroll naar beneden om nieuwe tickets te bekijken'
  273. 'Send': 'Verstuur'
  274. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  275. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  276. 'Start new conversation': 'Nieuw gesprek starten'
  277. 'Today': 'Vandaag'
  278. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': ''
  279. 'You are on waiting list position <strong>%s</strong>.': 'U bevindt zich op wachtlijstpositie <strong>%s</strong>.'
  280. 'pl':
  281. '<strong>Chat</strong> with us!': '<strong>Czatuj</strong> z nami!'
  282. 'All colleagues are busy.': 'Wszyscy agenci są zajęci.'
  283. 'Chat closed by %s': 'Chat zamknięty przez %s'
  284. 'Compose your message…': 'Skomponuj swoją wiadomość…'
  285. 'Connecting': 'Łączenie'
  286. 'Connection lost': 'Utracono połączenie'
  287. 'Connection re-established': 'Ponowne nawiązanie połączenia'
  288. 'Offline': 'Offline'
  289. 'Online': 'Online'
  290. 'Scroll down to see new messages': 'Skroluj w dół, aby zobaczyć wiadomości'
  291. 'Send': 'Wyślij'
  292. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  293. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  294. 'Start new conversation': 'Rozpocznij nową rozmowę'
  295. 'Today': 'Dzisiaj'
  296. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': ''
  297. 'You are on waiting list position <strong>%s</strong>.': 'Jesteś na pozycji listy oczekujących <strong>%s</strong>.'
  298. 'pt-br':
  299. '<strong>Chat</strong> with us!': '<strong>Converse</strong> conosco!'
  300. 'All colleagues are busy.': 'Nossos atendentes estão ocupados.'
  301. 'Chat closed by %s': 'Chat encerrado por %s'
  302. 'Compose your message…': 'Escreva sua mensagem…'
  303. 'Connecting': 'Conectando'
  304. 'Connection lost': 'Conexão perdida'
  305. 'Connection re-established': 'Conexão restabelecida'
  306. 'Offline': 'Desconectado'
  307. 'Online': 'Online'
  308. 'Scroll down to see new messages': 'Rolar para baixo para ver novas mensagems'
  309. 'Send': 'Enviar'
  310. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  311. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  312. 'Start new conversation': 'Iniciar uma nova conversa'
  313. 'Today': 'Hoje'
  314. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': ''
  315. 'You are on waiting list position <strong>%s</strong>.': 'Você está na posição <strong>%s</strong> da lista de espera.'
  316. 'ru':
  317. '<strong>Chat</strong> with us!': '<strong>Напишите</strong> нам!'
  318. 'All colleagues are busy.': 'Все коллеги заняты.'
  319. 'Chat closed by %s': 'Чат закрыт %s'
  320. 'Compose your message…': 'Составьте сообщение…'
  321. 'Connecting': 'Подключение'
  322. 'Connection lost': 'Подключение потеряно'
  323. 'Connection re-established': 'Подключение восстановлено'
  324. 'Offline': 'Оффлайн'
  325. 'Online': 'В сети'
  326. 'Scroll down to see new messages': 'Прокрутите вниз, чтобы увидеть новые сообщения'
  327. 'Send': 'Отправить'
  328. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  329. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  330. 'Start new conversation': 'Начать новую беседу'
  331. 'Today': 'Сегодня'
  332. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': ''
  333. 'You are on waiting list position <strong>%s</strong>.': 'Вы находитесь в списке ожидания <strong>%s</strong>.'
  334. 'sr':
  335. '<strong>Chat</strong> with us!': '<strong>Ћаскајте</strong> са нама!'
  336. 'All colleagues are busy.': 'Све колеге су заузете.'
  337. 'Chat closed by %s': 'Ћаскање затворено од стране %s'
  338. 'Compose your message…': 'Напишите поруку…'
  339. 'Connecting': 'Повезивање'
  340. 'Connection lost': 'Веза је изгубљена'
  341. 'Connection re-established': 'Веза је поново успостављена'
  342. 'Offline': 'Одсутан(а)'
  343. 'Online': 'Доступан(а)'
  344. 'Scroll down to see new messages': 'Скролујте на доле за нове поруке'
  345. 'Send': 'Пошаљи'
  346. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Пошто нисте одговорили у последњих %s минут(a), ваш разговор је завршен.'
  347. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Пошто нисте одговорили у последњих %s минут(a), ваш разговор са <strong>%s</strong> је завршен.'
  348. 'Start new conversation': 'Започни нови разговор'
  349. 'Today': 'Данас'
  350. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Жао нам је, добијање празног термина траје дуже од очекиваног. Молимо покушајте поново касније или нам пошаљите имејл поруку. Хвала вам!'
  351. 'You are on waiting list position <strong>%s</strong>.': 'Ви сте тренутно <strong>%s.</strong> у реду за чекање.'
  352. 'sr-latn-rs':
  353. '<strong>Chat</strong> with us!': '<strong>Ćaskajte</strong> sa nama!'
  354. 'All colleagues are busy.': 'Sve kolege su zauzete.'
  355. 'Chat closed by %s': 'Ćaskanje zatvoreno od strane %s'
  356. 'Compose your message…': 'Napišite poruku…'
  357. 'Connecting': 'Povezivanje'
  358. 'Connection lost': 'Veza je izgubljena'
  359. 'Connection re-established': 'Veza je ponovo uspostavljena'
  360. 'Offline': 'Odsutan(a)'
  361. 'Online': 'Dostupan(a)'
  362. 'Scroll down to see new messages': 'Skrolujte na dole za nove poruke'
  363. 'Send': 'Pošalji'
  364. '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.'
  365. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Pošto niste odgovorili u poslednjih %s minut(a), vaš razgovor sa <strong>%s</strong> je završen.'
  366. 'Start new conversation': 'Započni novi razgovor'
  367. 'Today': 'Danas'
  368. '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!'
  369. 'You are on waiting list position <strong>%s</strong>.': 'Vi ste trenutno <strong>%s.</strong> u redu za čekanje.'
  370. 'sv':
  371. '<strong>Chat</strong> with us!': '<strong>Chatta</strong> med oss!'
  372. 'All colleagues are busy.': 'Alla kollegor är upptagna.'
  373. 'Chat closed by %s': 'Chatt stängd av %s'
  374. 'Compose your message…': 'Skriv ditt meddelande …'
  375. 'Connecting': 'Ansluter'
  376. 'Connection lost': 'Anslutningen försvann'
  377. 'Connection re-established': 'Anslutningen återupprättas'
  378. 'Offline': 'Offline'
  379. 'Online': 'Online'
  380. 'Scroll down to see new messages': 'Bläddra ner för att se nya meddelanden'
  381. 'Send': 'Skicka'
  382. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  383. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  384. 'Start new conversation': 'Starta ny konversation'
  385. 'Today': 'Idag'
  386. '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!'
  387. 'You are on waiting list position <strong>%s</strong>.': 'Du är på väntelistan som position <strong>%s</strong>.'
  388. # ZAMMAD_TRANSLATIONS_END
  389. sessionId: undefined
  390. scrolledToBottom: true
  391. scrollSnapTolerance: 10
  392. richTextFormatKey:
  393. 66: true # b
  394. 73: true # i
  395. 85: true # u
  396. 83: true # s
  397. T: (string, items...) =>
  398. if @options.lang && @options.lang isnt 'en'
  399. if !@translations[@options.lang]
  400. @log.notice "Translation '#{@options.lang}' needed!"
  401. else
  402. translations = @translations[@options.lang]
  403. if !translations[string]
  404. @log.notice "Translation needed for '#{string}'"
  405. string = translations[string] || string
  406. if items
  407. for item in items
  408. string = string.replace(/%s/, item)
  409. string
  410. view: (name) =>
  411. return (options) =>
  412. if !options
  413. options = {}
  414. options.T = @T
  415. options.background = @options.background
  416. options.flat = @options.flat
  417. options.fontSize = @options.fontSize
  418. return window.zammadChatTemplates[name](options)
  419. constructor: (options) ->
  420. super(options)
  421. # jQuery migration
  422. if typeof jQuery != 'undefined' && @options.target instanceof jQuery
  423. @log.notice 'Chat: target option is a jQuery object. jQuery is not a requirement for the chat any more.'
  424. @options.target = @options.target.get(0)
  425. # fullscreen
  426. @isFullscreen = (window.matchMedia and window.matchMedia('(max-width: 768px)').matches)
  427. @scrollRoot = @getScrollRoot()
  428. # check prerequisites
  429. if !window.WebSocket or !sessionStorage
  430. @state = 'unsupported'
  431. @log.notice 'Chat: Browser not supported!'
  432. return
  433. if !@options.chatId
  434. @state = 'unsupported'
  435. @log.error 'Chat: need chatId as option!'
  436. return
  437. # detect language
  438. if !@options.lang
  439. @options.lang = document.documentElement.getAttribute('lang')
  440. if @options.lang
  441. if !@translations[@options.lang]
  442. @log.debug "lang: No #{@options.lang} found, try first two letters"
  443. @options.lang = @options.lang.replace(/-.+?$/, '') # replace "-xx" of xx-xx
  444. @log.debug "lang: #{@options.lang}"
  445. # detect host
  446. @detectHost() if !@options.host
  447. @loadCss()
  448. @io = new Io(@options)
  449. @io.set(
  450. onOpen: @render
  451. onClose: @onWebSocketClose
  452. onMessage: @onWebSocketMessage
  453. onError: @onError
  454. )
  455. @io.connect()
  456. getScrollRoot: ->
  457. return document.scrollingElement if 'scrollingElement' of document
  458. html = document.documentElement
  459. start = parseInt(html.pageYOffset, 10)
  460. html.pageYOffset = start + 1
  461. end = parseInt(html.pageYOffset, 10)
  462. html.pageYOffset = start
  463. return if end > start then html else document.body
  464. render: =>
  465. if !@el || !document.querySelector('.zammad-chat')
  466. @renderBase()
  467. # disable open button
  468. btn = document.querySelector(".#{ @options.buttonClass }")
  469. if btn
  470. btn.classList.add @options.inactiveClass
  471. @setAgentOnlineState 'online'
  472. @log.debug 'widget rendered'
  473. @startTimeoutObservers()
  474. @idleTimeout.start()
  475. # get current chat status
  476. @sessionId = sessionStorage.getItem('sessionId')
  477. @send 'chat_status_customer',
  478. session_id: @sessionId
  479. url: window.location.href
  480. renderBase: ->
  481. @el.remove() if @el
  482. @options.target.insertAdjacentHTML('beforeend', @view('chat')(
  483. title: @options.title,
  484. scrollHint: @options.scrollHint
  485. ))
  486. @el = @options.target.querySelector('.zammad-chat')
  487. @input = @el.querySelector('.zammad-chat-input')
  488. @body = @el.querySelector('.zammad-chat-body')
  489. # start bindings
  490. @el.querySelector('.js-chat-open').addEventListener('click', @open)
  491. @el.querySelector('.js-chat-toggle').addEventListener('click', @toggle)
  492. @el.querySelector('.js-chat-status').addEventListener('click', @stopPropagation)
  493. @el.querySelector('.zammad-chat-controls').addEventListener('submit', @onSubmit)
  494. @body.addEventListener('scroll', @detectScrolledtoBottom)
  495. @el.querySelector('.zammad-scroll-hint').addEventListener('click', @onScrollHintClick)
  496. @input.addEventListener('keydown', @onKeydown)
  497. @input.addEventListener('input', @onInput)
  498. @input.addEventListener('paste', @onPaste)
  499. @input.addEventListener('drop', @onDrop)
  500. window.addEventListener('beforeunload', @onLeaveTemporary)
  501. window.addEventListener('hashchange', =>
  502. if @isOpen
  503. if @sessionId
  504. @send 'chat_session_notice',
  505. session_id: @sessionId
  506. message: window.location.href
  507. return
  508. @idleTimeout.start()
  509. )
  510. stopPropagation: (event) ->
  511. event.stopPropagation()
  512. onDrop: (e) =>
  513. e.stopPropagation()
  514. e.preventDefault()
  515. if window.dataTransfer # ie
  516. dataTransfer = window.dataTransfer
  517. else if e.dataTransfer # other browsers
  518. dataTransfer = e.dataTransfer
  519. else
  520. throw 'No clipboardData support'
  521. x = e.clientX
  522. y = e.clientY
  523. file = dataTransfer.files[0]
  524. # look for images
  525. if file.type.match('image.*')
  526. reader = new FileReader()
  527. reader.onload = (e) =>
  528. # Insert the image at the carat
  529. insert = (dataUrl, width) =>
  530. # adapt image if we are on retina devices
  531. if @isRetina()
  532. width = width / 2
  533. result = dataUrl
  534. img = new Image()
  535. img.style.width = '100%'
  536. img.style.maxWidth = width + 'px'
  537. img.src = result
  538. if document.caretPositionFromPoint
  539. pos = document.caretPositionFromPoint(x, y)
  540. range = document.createRange()
  541. range.setStart(pos.offsetNode, pos.offset)
  542. range.collapse()
  543. range.insertNode(img)
  544. else if document.caretRangeFromPoint
  545. range = document.caretRangeFromPoint(x, y)
  546. range.insertNode(img)
  547. else
  548. console.log('could not find carat')
  549. # resize if to big
  550. @resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
  551. reader.readAsDataURL(file)
  552. onPaste: (e) =>
  553. e.stopPropagation()
  554. e.preventDefault()
  555. if e.clipboardData
  556. clipboardData = e.clipboardData
  557. else if window.clipboardData
  558. clipboardData = window.clipboardData
  559. else if e.clipboardData
  560. clipboardData = e.clipboardData
  561. else
  562. throw 'No clipboardData support'
  563. imageInserted = false
  564. if clipboardData && clipboardData.items && clipboardData.items[0]
  565. item = clipboardData.items[0]
  566. if item.kind == 'file' && (item.type == 'image/png' || item.type == 'image/jpeg')
  567. imageFile = item.getAsFile()
  568. reader = new FileReader()
  569. reader.onload = (e) =>
  570. insert = (dataUrl, width) =>
  571. # adapt image if we are on retina devices
  572. if @isRetina()
  573. width = width / 2
  574. img = new Image()
  575. img.style.width = '100%'
  576. img.style.maxWidth = width + 'px'
  577. img.src = dataUrl
  578. document.execCommand('insertHTML', false, img)
  579. # resize if to big
  580. @resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
  581. reader.readAsDataURL(imageFile)
  582. imageInserted = true
  583. return if imageInserted
  584. # check existing + paste text for limit
  585. text = undefined
  586. docType = undefined
  587. try
  588. text = clipboardData.getData('text/html')
  589. docType = 'html'
  590. if !text || text.length is 0
  591. docType = 'text'
  592. text = clipboardData.getData('text/plain')
  593. if !text || text.length is 0
  594. docType = 'text2'
  595. text = clipboardData.getData('text')
  596. catch e
  597. console.log('Sorry, can\'t insert markup because browser is not supporting it.')
  598. docType = 'text3'
  599. text = clipboardData.getData('text')
  600. if docType is 'text' || docType is 'text2' || docType is 'text3'
  601. text = '<div>' + text.replace(/\n/g, '</div><div>') + '</div>'
  602. text = text.replace(/<div><\/div>/g, '<div><br></div>')
  603. console.log('p', docType, text)
  604. if docType is 'html'
  605. html = document.createElement('div')
  606. # can't log because might contain malicious content
  607. # @log.debug 'HTML clipboard', text
  608. sanitized = DOMPurify.sanitize(text)
  609. @log.debug 'sanitized HTML clipboard', sanitized
  610. html.innerHTML = sanitized
  611. match = false
  612. htmlTmp = text
  613. regex = new RegExp('<(/w|w)\:[A-Za-z]')
  614. if htmlTmp.match(regex)
  615. match = true
  616. htmlTmp = htmlTmp.replace(regex, '')
  617. regex = new RegExp('<(/o|o)\:[A-Za-z]')
  618. if htmlTmp.match(regex)
  619. match = true
  620. htmlTmp = htmlTmp.replace(regex, '')
  621. if match
  622. html = @wordFilter(html)
  623. #html
  624. for node in html.childNodes
  625. if node.nodeType == 8
  626. node.remove()
  627. # remove tags, keep content
  628. for node in html.querySelectorAll('a, font, small, time, form, label')
  629. node.outerHTML = node.innerHTML
  630. # replace tags with generic div
  631. # New type of the tag
  632. replacementTag = 'div';
  633. # Replace all x tags with the type of replacementTag
  634. for node in html.querySelectorAll('textarea')
  635. outer = node.outerHTML
  636. # Replace opening tag
  637. regex = new RegExp('<' + node.tagName, 'i')
  638. newTag = outer.replace(regex, '<' + replacementTag)
  639. # Replace closing tag
  640. regex = new RegExp('</' + node.tagName, 'i')
  641. newTag = newTag.replace(regex, '</' + replacementTag)
  642. node.outerHTML = newTag
  643. # remove tags & content
  644. for node in html.querySelectorAll('font, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset')
  645. node.remove()
  646. @removeAttributes(html)
  647. text = html.innerHTML
  648. # as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower)
  649. if docType is 'text3'
  650. @pasteHtmlAtCaret(text)
  651. else
  652. document.execCommand('insertHTML', false, text)
  653. true
  654. onKeydown: (e) =>
  655. # check for enter
  656. if not @inputDisabled and not e.shiftKey and e.keyCode is 13
  657. e.preventDefault()
  658. @sendMessage()
  659. richtTextControl = false
  660. if !e.altKey && !e.ctrlKey && e.metaKey
  661. richtTextControl = true
  662. else if !e.altKey && e.ctrlKey && !e.metaKey
  663. richtTextControl = true
  664. if richtTextControl && @richTextFormatKey[ e.keyCode ]
  665. e.preventDefault()
  666. if e.keyCode is 66
  667. document.execCommand('bold')
  668. return true
  669. if e.keyCode is 73
  670. document.execCommand('italic')
  671. return true
  672. if e.keyCode is 85
  673. document.execCommand('underline')
  674. return true
  675. if e.keyCode is 83
  676. document.execCommand('strikeThrough')
  677. return true
  678. send: (event, data = {}) =>
  679. data.chat_id = @options.chatId
  680. @io.send(event, data)
  681. onWebSocketMessage: (pipes) =>
  682. for pipe in pipes
  683. @log.debug 'ws:onmessage', pipe
  684. switch pipe.event
  685. when 'chat_error'
  686. @log.notice pipe.data
  687. if pipe.data && pipe.data.state is 'chat_disabled'
  688. @destroy(remove: true)
  689. when 'chat_session_message'
  690. return if pipe.data.self_written
  691. @receiveMessage pipe.data
  692. when 'chat_session_typing'
  693. return if pipe.data.self_written
  694. @onAgentTypingStart()
  695. when 'chat_session_start'
  696. @onConnectionEstablished pipe.data
  697. when 'chat_session_queue'
  698. @onQueueScreen pipe.data
  699. when 'chat_session_closed'
  700. @onSessionClosed pipe.data
  701. when 'chat_session_left'
  702. @onSessionClosed pipe.data
  703. when 'chat_status_customer'
  704. switch pipe.data.state
  705. when 'online'
  706. @sessionId = undefined
  707. if !@options.cssAutoload || @cssLoaded
  708. @onReady()
  709. else
  710. @socketReady = true
  711. when 'offline'
  712. @onError 'Zammad Chat: No agent online'
  713. when 'chat_disabled'
  714. @onError 'Zammad Chat: Chat is disabled'
  715. when 'no_seats_available'
  716. @onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}"
  717. when 'reconnect'
  718. @onReopenSession pipe.data
  719. onReady: ->
  720. @log.debug 'widget ready for use'
  721. btn = document.querySelector(".#{ @options.buttonClass }")
  722. if btn
  723. btn.addEventListener('click', @open)
  724. btn.classList.remove(@options.inactiveClass)
  725. @options.onReady?()
  726. if @options.show
  727. @show()
  728. onError: (message) =>
  729. @log.debug message
  730. @addStatus(message)
  731. btn = document.querySelector(".#{ @options.buttonClass }")
  732. if btn
  733. btn.classList.add('zammad-chat-is-hidden')
  734. if @isOpen
  735. @disableInput()
  736. @destroy(remove: false)
  737. else
  738. @destroy(remove: true)
  739. @options.onError?(message)
  740. onReopenSession: (data) =>
  741. @log.debug 'old messages', data.session
  742. @inactiveTimeout.start()
  743. unfinishedMessage = sessionStorage.getItem 'unfinished_message'
  744. # rerender chat history
  745. if data.agent
  746. @onConnectionEstablished(data)
  747. for message in data.session
  748. @renderMessage
  749. message: message.content
  750. id: message.id
  751. from: if message.created_by_id then 'agent' else 'customer'
  752. if unfinishedMessage
  753. @input.innerHTML = unfinishedMessage
  754. # show wait list
  755. if data.position
  756. @onQueue data
  757. @show()
  758. @open()
  759. @scrollToBottom()
  760. if unfinishedMessage
  761. @input.focus()
  762. onInput: =>
  763. # remove unread-state from messages
  764. for message in @el.querySelectorAll('.zammad-chat-message--unread')
  765. message.classList.remove 'zammad-chat-message--unread'
  766. sessionStorage.setItem 'unfinished_message', @input.innerHTML
  767. @onTyping()
  768. onTyping: ->
  769. # send typing start event only every 1.5 seconds
  770. return if @isTyping && @isTyping > new Date(new Date().getTime() - 1500)
  771. @isTyping = new Date()
  772. @send 'chat_session_typing',
  773. session_id: @sessionId
  774. @inactiveTimeout.start()
  775. onSubmit: (event) =>
  776. event.preventDefault()
  777. @sendMessage()
  778. sendMessage: ->
  779. message = @input.innerHTML
  780. return if !message
  781. @inactiveTimeout.start()
  782. sessionStorage.removeItem 'unfinished_message'
  783. messageElement = @view('message')
  784. message: message
  785. from: 'customer'
  786. id: @_messageCount++
  787. unreadClass: ''
  788. @maybeAddTimestamp()
  789. # add message before message typing loader
  790. if @el.querySelector('.zammad-chat-message--typing')
  791. @lastAddedType = 'typing-placeholder'
  792. @el.querySelector('.zammad-chat-message--typing').insertAdjacentHTML('beforebegin', messageElement)
  793. else
  794. @lastAddedType = 'message--customer'
  795. @body.insertAdjacentHTML('beforeend', messageElement)
  796. @input.innerHTML = ''
  797. @scrollToBottom()
  798. # send message event
  799. @send 'chat_session_message',
  800. content: message
  801. id: @_messageCount
  802. session_id: @sessionId
  803. receiveMessage: (data) =>
  804. @inactiveTimeout.start()
  805. # hide writing indicator
  806. @onAgentTypingEnd()
  807. @maybeAddTimestamp()
  808. @renderMessage
  809. message: data.message.content
  810. id: data.id
  811. from: 'agent'
  812. @scrollToBottom showHint: true
  813. renderMessage: (data) =>
  814. @lastAddedType = "message--#{ data.from }"
  815. data.unreadClass = if document.hidden then ' zammad-chat-message--unread' else ''
  816. @body.insertAdjacentHTML('beforeend', @view('message')(data))
  817. open: =>
  818. if @isOpen
  819. @log.debug 'widget already open, block'
  820. return
  821. @isOpen = true
  822. @log.debug 'open widget'
  823. @show()
  824. if !@sessionId
  825. @showLoader()
  826. @el.classList.add 'zammad-chat-is-open'
  827. remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight
  828. @el.style.transform = "translateY(#{remainerHeight}px)"
  829. # force redraw
  830. @el.clientHeight
  831. if !@sessionId
  832. @el.addEventListener 'transitionend', @onOpenAnimationEnd
  833. @el.classList.add 'zammad-chat--animate'
  834. # force redraw
  835. @el.clientHeight
  836. # start animation
  837. @el.style.transform = ''
  838. @send('chat_session_init'
  839. url: window.location.href
  840. )
  841. else
  842. @el.style.transform = ''
  843. @onOpenAnimationEnd()
  844. onOpenAnimationEnd: =>
  845. @el.removeEventListener 'transitionend', @onOpenAnimationEnd
  846. @el.classList.remove 'zammad-chat--animate'
  847. @idleTimeout.stop()
  848. if @isFullscreen
  849. @disableScrollOnRoot()
  850. @options.onOpenAnimationEnd?()
  851. sessionClose: =>
  852. # send close
  853. @send 'chat_session_close',
  854. session_id: @sessionId
  855. # stop timer
  856. @inactiveTimeout.stop()
  857. @waitingListTimeout.stop()
  858. # delete input store
  859. sessionStorage.removeItem 'unfinished_message'
  860. # stop delay of initial queue position
  861. if @onInitialQueueDelayId
  862. clearTimeout(@onInitialQueueDelayId)
  863. @setSessionId undefined
  864. toggle: (event) =>
  865. if @isOpen
  866. @close(event)
  867. else
  868. @open(event)
  869. close: (event) =>
  870. if !@isOpen
  871. @log.debug 'can\'t close widget, it\'s not open'
  872. return
  873. if @initDelayId
  874. clearTimeout(@initDelayId)
  875. if @sessionId
  876. @log.debug 'session close before widget close'
  877. @sessionClose()
  878. @log.debug 'close widget'
  879. event.stopPropagation() if event
  880. if @isFullscreen
  881. @enableScrollOnRoot()
  882. # close window
  883. remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight
  884. @el.addEventListener 'transitionend', @onCloseAnimationEnd
  885. @el.classList.add 'zammad-chat--animate'
  886. # force redraw
  887. document.offsetHeight
  888. # animate out
  889. @el.style.transform = "translateY(#{remainerHeight}px)"
  890. onCloseAnimationEnd: =>
  891. @el.removeEventListener 'transitionend', @onCloseAnimationEnd
  892. @el.classList.remove 'zammad-chat-is-open', 'zammad-chat--animate'
  893. @el.style.transform = ''
  894. @showLoader()
  895. @el.querySelector('.zammad-chat-welcome').classList.remove('zammad-chat-is-hidden')
  896. @el.querySelector('.zammad-chat-agent').classList.add('zammad-chat-is-hidden')
  897. @el.querySelector('.zammad-chat-agent-status').classList.add('zammad-chat-is-hidden')
  898. @isOpen = false
  899. @options.onCloseAnimationEnd?()
  900. @io.reconnect()
  901. onWebSocketClose: =>
  902. return if @isOpen
  903. if @el
  904. @el.classList.remove('zammad-chat-is-shown')
  905. @el.classList.remove('zammad-chat-is-loaded')
  906. show: ->
  907. return if @state is 'offline'
  908. @el.classList.add('zammad-chat-is-loaded')
  909. @el.classList.add('zammad-chat-is-shown')
  910. disableInput: ->
  911. @inputDisabled = true
  912. @input.setAttribute('contenteditable', false)
  913. @el.querySelector('.zammad-chat-send').disabled = true
  914. @io.close()
  915. enableInput: ->
  916. @inputDisabled = false
  917. @input.setAttribute('contenteditable', true)
  918. @el.querySelector('.zammad-chat-send').disabled = false
  919. hideModal: ->
  920. @el.querySelector('.zammad-chat-modal').innerHTML = ''
  921. onQueueScreen: (data) =>
  922. @setSessionId data.session_id
  923. # delay initial queue position, show connecting first
  924. show = =>
  925. @onQueue data
  926. @waitingListTimeout.start()
  927. if @initialQueueDelay && !@onInitialQueueDelayId
  928. @onInitialQueueDelayId = setTimeout(show, @initialQueueDelay)
  929. return
  930. # stop delay of initial queue position
  931. if @onInitialQueueDelayId
  932. clearTimeout(@onInitialQueueDelayId)
  933. # show queue position
  934. show()
  935. onQueue: (data) =>
  936. @log.notice 'onQueue', data.position
  937. @inQueue = true
  938. @el.querySelector('.zammad-chat-modal').innerHTML = @view('waiting')
  939. position: data.position
  940. onAgentTypingStart: =>
  941. if @stopTypingId
  942. clearTimeout(@stopTypingId)
  943. @stopTypingId = setTimeout(@onAgentTypingEnd, 3000)
  944. # never display two typing indicators
  945. return if @el.querySelector('.zammad-chat-message--typing')
  946. @maybeAddTimestamp()
  947. @body.insertAdjacentHTML('beforeend', @view('typingIndicator')())
  948. # only if typing indicator is shown
  949. return if !@isVisible(@el.querySelector('.zammad-chat-message--typing'), true)
  950. @scrollToBottom()
  951. onAgentTypingEnd: =>
  952. @el.querySelector('.zammad-chat-message--typing').remove() if @el.querySelector('.zammad-chat-message--typing')
  953. onLeaveTemporary: =>
  954. return if !@sessionId
  955. @send 'chat_session_leave_temporary',
  956. session_id: @sessionId
  957. maybeAddTimestamp: ->
  958. timestamp = Date.now()
  959. if !@lastTimestamp or (timestamp - @lastTimestamp) > @showTimeEveryXMinutes * 60000
  960. label = @T('Today')
  961. time = new Date().toTimeString().substr 0,5
  962. if @lastAddedType is 'timestamp'
  963. # update last time
  964. @updateLastTimestamp label, time
  965. @lastTimestamp = timestamp
  966. else
  967. # add new timestamp
  968. @body.insertAdjacentHTML 'beforeend', @view('timestamp')
  969. label: label
  970. time: time
  971. @lastTimestamp = timestamp
  972. @lastAddedType = 'timestamp'
  973. @scrollToBottom()
  974. updateLastTimestamp: (label, time) ->
  975. return if !@el
  976. timestamps = @el.querySelectorAll('.zammad-chat-body .zammad-chat-timestamp')
  977. return if !timestamps
  978. timestamps[timestamps.length - 1].outerHTML = @view('timestamp')
  979. label: label
  980. time: time
  981. addStatus: (status) ->
  982. return if !@el
  983. @maybeAddTimestamp()
  984. @body.insertAdjacentHTML 'beforeend', @view('status')
  985. status: status
  986. @scrollToBottom()
  987. detectScrolledtoBottom: =>
  988. scrollBottom = @body.scrollTop + @body.offsetHeight
  989. @scrolledToBottom = Math.abs(scrollBottom - @body.scrollHeight) <= @scrollSnapTolerance
  990. @el.querySelector('.zammad-scroll-hint').classList.add('is-hidden') if @scrolledToBottom
  991. showScrollHint: ->
  992. @el.querySelector('.zammad-scroll-hint').classList.remove('is-hidden')
  993. # compensate scroll
  994. @body.scrollTop = @body.scrollTop + @el.querySelector('.zammad-scroll-hint').offsetHeight
  995. onScrollHintClick: =>
  996. # animate scroll
  997. @body.scrollTo
  998. top: @body.scrollHeight
  999. behavior: 'smooth'
  1000. scrollToBottom: ({ showHint } = { showHint: false }) ->
  1001. if @scrolledToBottom
  1002. @body.scrollTop = @body.scrollHeight
  1003. else if showHint
  1004. @showScrollHint()
  1005. destroy: (params = {}) =>
  1006. @log.debug 'destroy widget', params
  1007. @setAgentOnlineState 'offline'
  1008. if params.remove && @el
  1009. @el.remove()
  1010. # Remove button, because it can no longer be used.
  1011. btn = document.querySelector(".#{ @options.buttonClass }")
  1012. if btn
  1013. btn.classList.add @options.inactiveClass
  1014. btn.style.display = 'none';
  1015. # stop all timer
  1016. if @waitingListTimeout
  1017. @waitingListTimeout.stop()
  1018. if @inactiveTimeout
  1019. @inactiveTimeout.stop()
  1020. if @idleTimeout
  1021. @idleTimeout.stop()
  1022. # stop ws connection
  1023. @io.close()
  1024. reconnect: =>
  1025. # set status to connecting
  1026. @log.notice 'reconnecting'
  1027. @disableInput()
  1028. @lastAddedType = 'status'
  1029. @setAgentOnlineState 'connecting'
  1030. @addStatus @T('Connection lost')
  1031. onConnectionReestablished: =>
  1032. # set status back to online
  1033. @lastAddedType = 'status'
  1034. @setAgentOnlineState 'online'
  1035. @addStatus @T('Connection re-established')
  1036. @options.onConnectionReestablished?()
  1037. onSessionClosed: (data) ->
  1038. @addStatus @T('Chat closed by %s', data.realname)
  1039. @disableInput()
  1040. @setAgentOnlineState 'offline'
  1041. @inactiveTimeout.stop()
  1042. @options.onSessionClosed?(data)
  1043. setSessionId: (id) =>
  1044. @sessionId = id
  1045. if id is undefined
  1046. sessionStorage.removeItem 'sessionId'
  1047. else
  1048. sessionStorage.setItem 'sessionId', id
  1049. onConnectionEstablished: (data) =>
  1050. # stop delay of initial queue position
  1051. if @onInitialQueueDelayId
  1052. clearTimeout @onInitialQueueDelayId
  1053. @inQueue = false
  1054. if data.agent
  1055. @agent = data.agent
  1056. if data.session_id
  1057. @setSessionId data.session_id
  1058. # empty old messages
  1059. @body.innerHTML = ''
  1060. @el.querySelector('.zammad-chat-agent').innerHTML = @view('agent')
  1061. agent: @agent
  1062. @enableInput()
  1063. @hideModal()
  1064. @el.querySelector('.zammad-chat-welcome').classList.add('zammad-chat-is-hidden')
  1065. @el.querySelector('.zammad-chat-agent').classList.remove('zammad-chat-is-hidden')
  1066. @el.querySelector('.zammad-chat-agent-status').classList.remove('zammad-chat-is-hidden')
  1067. @input.focus() if not @isFullscreen
  1068. @setAgentOnlineState 'online'
  1069. @waitingListTimeout.stop()
  1070. @idleTimeout.stop()
  1071. @inactiveTimeout.start()
  1072. @options.onConnectionEstablished?(data)
  1073. showCustomerTimeout: ->
  1074. @el.querySelector('.zammad-chat-modal').innerHTML = @view('customer_timeout')
  1075. agent: @agent.name
  1076. delay: @options.inactiveTimeout
  1077. @el.querySelector('.js-restart').addEventListener 'click', -> location.reload()
  1078. @sessionClose()
  1079. showWaitingListTimeout: ->
  1080. @el.querySelector('.zammad-chat-modal').innerHTML = @view('waiting_list_timeout')
  1081. delay: @options.watingListTimeout
  1082. @el.querySelector('.js-restart').addEventListener 'click', -> location.reload()
  1083. @sessionClose()
  1084. showLoader: ->
  1085. @el.querySelector('.zammad-chat-modal').innerHTML = @view('loader')()
  1086. setAgentOnlineState: (state) =>
  1087. @state = state
  1088. return if !@el
  1089. capitalizedState = state.charAt(0).toUpperCase() + state.slice(1)
  1090. @el.querySelector('.zammad-chat-agent-status').dataset.status = state
  1091. @el.querySelector('.zammad-chat-agent-status').textContent = @T(capitalizedState)
  1092. detectHost: ->
  1093. protocol = 'ws://'
  1094. if scriptProtocol is 'https'
  1095. protocol = 'wss://'
  1096. @options.host = "#{ protocol }#{ scriptHost }/ws"
  1097. loadCss: ->
  1098. return if !@options.cssAutoload
  1099. url = @options.cssUrl
  1100. if !url
  1101. url = @options.host
  1102. .replace(/^wss/i, 'https')
  1103. .replace(/^ws/i, 'http')
  1104. .replace(/\/ws$/i, '') # WebSocket may run on example.com/ws path
  1105. url += '/assets/chat/chat.css'
  1106. @log.debug "load css from '#{url}'"
  1107. styles = "@import url('#{url}');"
  1108. newSS = document.createElement('link')
  1109. newSS.onload = @onCssLoaded
  1110. newSS.rel = 'stylesheet'
  1111. newSS.href = 'data:text/css,' + escape(styles)
  1112. document.getElementsByTagName('head')[0].appendChild(newSS)
  1113. onCssLoaded: =>
  1114. @cssLoaded = true
  1115. if @socketReady
  1116. @onReady()
  1117. @options.onCssLoaded?()
  1118. startTimeoutObservers: =>
  1119. @idleTimeout = new Timeout(
  1120. logPrefix: 'idleTimeout'
  1121. debug: @options.debug
  1122. timeout: @options.idleTimeout
  1123. timeoutIntervallCheck: @options.idleTimeoutIntervallCheck
  1124. callback: =>
  1125. @log.debug 'Idle timeout reached, hide widget', new Date
  1126. @destroy(remove: true)
  1127. )
  1128. @inactiveTimeout = new Timeout(
  1129. logPrefix: 'inactiveTimeout'
  1130. debug: @options.debug
  1131. timeout: @options.inactiveTimeout
  1132. timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck
  1133. callback: =>
  1134. @log.debug 'Inactive timeout reached, show timeout screen.', new Date
  1135. @showCustomerTimeout()
  1136. @destroy(remove: false)
  1137. )
  1138. @waitingListTimeout = new Timeout(
  1139. logPrefix: 'waitingListTimeout'
  1140. debug: @options.debug
  1141. timeout: @options.waitingListTimeout
  1142. timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck
  1143. callback: =>
  1144. @log.debug 'Waiting list timeout reached, show timeout screen.', new Date
  1145. @showWaitingListTimeout()
  1146. @destroy(remove: false)
  1147. )
  1148. disableScrollOnRoot: ->
  1149. @rootScrollOffset = @scrollRoot.scrollTop
  1150. @scrollRoot.style.overflow = 'hidden'
  1151. @scrollRoot.style.position = 'fixed'
  1152. enableScrollOnRoot: ->
  1153. @scrollRoot.scrollTop = @rootScrollOffset
  1154. @scrollRoot.style.overflow = ''
  1155. @scrollRoot.style.position = ''
  1156. # based on https://github.com/customd/jquery-visible/blob/master/jquery.visible.js
  1157. # to have not dependency, port to coffeescript
  1158. isVisible: (el, partial, hidden, direction) ->
  1159. return if el.length < 1
  1160. vpWidth = window.innerWidth
  1161. vpHeight = window.innerHeight
  1162. direction = if direction then direction else 'both'
  1163. clientSize = if hidden is true then t.offsetWidth * t.offsetHeight else true
  1164. rec = el.getBoundingClientRect()
  1165. tViz = rec.top >= 0 && rec.top < vpHeight
  1166. bViz = rec.bottom > 0 && rec.bottom <= vpHeight
  1167. lViz = rec.left >= 0 && rec.left < vpWidth
  1168. rViz = rec.right > 0 && rec.right <= vpWidth
  1169. vVisible = if partial then tViz || bViz else tViz && bViz
  1170. hVisible = if partial then lViz || rViz else lViz && rViz
  1171. if direction is 'both'
  1172. return clientSize && vVisible && hVisible
  1173. else if direction is 'vertical'
  1174. return clientSize && vVisible
  1175. else if direction is 'horizontal'
  1176. return clientSize && hVisible
  1177. isRetina: ->
  1178. if window.matchMedia
  1179. 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)')
  1180. return (mq && mq.matches || (window.devicePixelRatio > 1))
  1181. false
  1182. resizeImage: (dataURL, x = 'auto', y = 'auto', sizeFactor = 1, type, quallity, callback, force = true) ->
  1183. # load image from data url
  1184. imageObject = new Image()
  1185. imageObject.onload = ->
  1186. imageWidth = imageObject.width
  1187. imageHeight = imageObject.height
  1188. console.log('ImageService', 'current size', imageWidth, imageHeight)
  1189. if y is 'auto' && x is 'auto'
  1190. x = imageWidth
  1191. y = imageHeight
  1192. # get auto dimensions
  1193. if y is 'auto'
  1194. factor = imageWidth / x
  1195. y = imageHeight / factor
  1196. if x is 'auto'
  1197. factor = imageWidth / y
  1198. x = imageHeight / factor
  1199. # check if resize is needed
  1200. resize = false
  1201. if x < imageWidth || y < imageHeight
  1202. resize = true
  1203. x = x * sizeFactor
  1204. y = y * sizeFactor
  1205. else
  1206. x = imageWidth
  1207. y = imageHeight
  1208. # create canvas and set dimensions
  1209. canvas = document.createElement('canvas')
  1210. canvas.width = x
  1211. canvas.height = y
  1212. # draw image on canvas and set image dimensions
  1213. context = canvas.getContext('2d')
  1214. context.drawImage(imageObject, 0, 0, x, y)
  1215. # set quallity based on image size
  1216. if quallity == 'auto'
  1217. if x < 200 && y < 200
  1218. quallity = 1
  1219. else if x < 400 && y < 400
  1220. quallity = 0.9
  1221. else if x < 600 && y < 600
  1222. quallity = 0.8
  1223. else if x < 900 && y < 900
  1224. quallity = 0.7
  1225. else
  1226. quallity = 0.6
  1227. # execute callback with resized image
  1228. newDataUrl = canvas.toDataURL(type, quallity)
  1229. if resize
  1230. console.log('ImageService', 'resize', x/sizeFactor, y/sizeFactor, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
  1231. callback(newDataUrl, x/sizeFactor, y/sizeFactor, true)
  1232. return
  1233. console.log('ImageService', 'no resize', x, y, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
  1234. callback(newDataUrl, x, y, false)
  1235. # load image from data url
  1236. imageObject.src = dataURL
  1237. # taken from https://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
  1238. pasteHtmlAtCaret: (html) ->
  1239. sel = undefined
  1240. range = undefined
  1241. if window.getSelection
  1242. sel = window.getSelection()
  1243. if sel.getRangeAt && sel.rangeCount
  1244. range = sel.getRangeAt(0)
  1245. range.deleteContents()
  1246. el = document.createElement('div')
  1247. el.innerHTML = html
  1248. frag = document.createDocumentFragment(node, lastNode)
  1249. while node = el.firstChild
  1250. lastNode = frag.appendChild(node)
  1251. range.insertNode(frag)
  1252. if lastNode
  1253. range = range.cloneRange()
  1254. range.setStartAfter(lastNode)
  1255. range.collapse(true)
  1256. sel.removeAllRanges()
  1257. sel.addRange(range)
  1258. else if document.selection && document.selection.type != 'Control'
  1259. document.selection.createRange().pasteHTML(html)
  1260. # (C) sbrin - https://github.com/sbrin
  1261. # https://gist.github.com/sbrin/6801034
  1262. wordFilter: (editor) ->
  1263. content = editor.html()
  1264. # Word comments like conditional comments etc
  1265. content = content.replace(/<!--[\s\S]+?-->/gi, '')
  1266. # Remove comments, scripts (e.g., msoShowComment), XML tag, VML content,
  1267. # MS Office namespaced tags, and a few other tags
  1268. content = content.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, '')
  1269. # Convert <s> into <strike> for line-though
  1270. content = content.replace(/<(\/?)s>/gi, '<$1strike>')
  1271. # Replace nbsp entites to char since it's easier to handle
  1272. # content = content.replace(/&nbsp;/gi, "\u00a0")
  1273. content = content.replace(/&nbsp;/gi, ' ')
  1274. # Convert <span style="mso-spacerun:yes">___</span> to string of alternating
  1275. # breaking/non-breaking spaces of same length
  1276. #content = content.replace(/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi, (str, spaces) ->
  1277. # return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : ''
  1278. #)
  1279. editor.innerHTML = content
  1280. # Parse out list indent level for lists
  1281. for p in editor.querySelectorAll('p')
  1282. str = p.getAttribute('style')
  1283. matches = /mso-list:\w+ \w+([0-9]+)/.exec(str)
  1284. if matches
  1285. p.dataset._listLevel = parseInt(matches[1], 10)
  1286. # Parse Lists
  1287. last_level = 0
  1288. pnt = null
  1289. for p in editor.querySelectorAll('p')
  1290. cur_level = p.dataset._listLevel
  1291. if cur_level != undefined
  1292. txt = p.textContent
  1293. list_tag = '<ul></ul>'
  1294. if (/^\s*\w+\./.test(txt))
  1295. matches = /([0-9])\./.exec(txt)
  1296. if matches
  1297. start = parseInt(matches[1], 10)
  1298. list_tag = start>1 ? '<ol start="' + start + '"></ol>' : '<ol></ol>'
  1299. else
  1300. list_tag = '<ol></ol>'
  1301. if cur_level > last_level
  1302. if last_level == 0
  1303. p.insertAdjacentHTML 'beforebegin', list_tag
  1304. pnt = p.previousElementSibling
  1305. else
  1306. pnt.insertAdjacentHTML 'beforeend', list_tag
  1307. if cur_level < last_level
  1308. for i in [i..last_level-cur_level]
  1309. pnt = pnt.parentNode
  1310. p.querySelector('span:first').remove() if p.querySelector('span:first')
  1311. pnt.insertAdjacentHTML 'beforeend', '<li>' + p.innerHTML + '</li>'
  1312. p.remove()
  1313. last_level = cur_level
  1314. else
  1315. last_level = 0
  1316. el.removeAttribute('style') for el in editor.querySelectorAll('[style]')
  1317. el.removeAttribute('align') for el in editor.querySelectorAll('[align]')
  1318. el.outerHTML = el.innerHTML for el in editor.querySelectorAll('span')
  1319. el.remove() for el in editor.querySelectorAll('span:empty')
  1320. el.removeAttribute('class') for el in editor.querySelectorAll("[class^='Mso']")
  1321. el.remove() for el in editor.querySelectorAll('p:empty')
  1322. editor
  1323. removeAttribute: (element) ->
  1324. return if !element
  1325. for att in element.attributes
  1326. element.removeAttribute(att.name)
  1327. removeAttributes: (html) =>
  1328. for node in html.querySelectorAll('*')
  1329. @removeAttribute node
  1330. html
  1331. window.ZammadChat = ZammadChat