chat.coffee 54 KB

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