chat-no-jquery.coffee 68 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650
  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. 'da':
  172. '<strong>Chat</strong> with us!': '<strong>Chat</strong> med os!'
  173. 'Scroll down to see new messages': 'Scroll ned for at se nye beskeder'
  174. 'Online': 'Online'
  175. 'Offline': 'Offline'
  176. 'Connecting': 'Forbinder'
  177. 'Connection re-established': 'Forbindelse genoprettet'
  178. 'Today': 'I dag'
  179. 'Send': 'Send'
  180. 'Chat closed by %s': 'Chat lukket af %s'
  181. 'Compose your message...': 'Skriv en besked...'
  182. 'All colleagues are busy.': 'Alle kollegaer er optaget.'
  183. 'You are on waiting list position <strong>%s</strong>.': 'Du er i venteliste som nummer <strong>%s</strong>.'
  184. 'Start new conversation': 'Start en ny samtale'
  185. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Da du ikke har svaret i de sidste %s minutter er din samtale med <strong>%s</strong> blevet lukket.'
  186. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Da du ikke har svaret i de sidste %s minutter er din samtale blevet lukket.'
  187. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Vi beklager, det tager længere end forventet at få en ledig plads. Prøv venligst igen senere eller send os en e-mail. På forhånd tak!'
  188. 'de':
  189. '<strong>Chat</strong> with us!': '<strong>Chatte</strong> mit uns!'
  190. 'Scroll down to see new messages': 'Scrolle nach unten um neue Nachrichten zu sehen'
  191. 'Online': 'Online'
  192. 'Offline': 'Offline'
  193. 'Connecting': 'Verbinden'
  194. 'Connection re-established': 'Verbindung wiederhergestellt'
  195. 'Today': 'Heute'
  196. 'Send': 'Senden'
  197. 'Chat closed by %s': 'Chat beendet von %s'
  198. 'Compose your message...': 'Ihre Nachricht...'
  199. 'All colleagues are busy.': 'Alle Kollegen sind belegt.'
  200. 'You are on waiting list position <strong>%s</strong>.': 'Sie sind in der Warteliste an der Position <strong>%s</strong>.'
  201. 'Start new conversation': 'Neue Konversation starten'
  202. '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.'
  203. '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.'
  204. '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!'
  205. 'es':
  206. '<strong>Chat</strong> with us!': '<strong>Chatee</strong> con nosotros!'
  207. 'Scroll down to see new messages': 'Haga scroll hacia abajo para ver nuevos mensajes'
  208. 'Online': 'En linea'
  209. 'Offline': 'Desconectado'
  210. 'Connecting': 'Conectando'
  211. 'Connection re-established': 'Conexión restablecida'
  212. 'Today': 'Hoy'
  213. 'Send': 'Enviar'
  214. 'Chat closed by %s': 'Chat cerrado por %s'
  215. 'Compose your message...': 'Escriba su mensaje...'
  216. 'All colleagues are busy.': 'Todos los agentes están ocupados.'
  217. 'You are on waiting list position <strong>%s</strong>.': 'Usted está en la posición <strong>%s</strong> de la lista de espera.'
  218. 'Start new conversation': 'Iniciar nueva conversación'
  219. '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.'
  220. '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.'
  221. '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!'
  222. 'fi':
  223. '<strong>Chat</strong> with us!': '<strong>Keskustele</strong> kanssamme!'
  224. 'Scroll down to see new messages': 'Rullaa alas nähdäksesi uudet viestit'
  225. 'Online': 'Paikalla'
  226. 'Offline': 'Poissa'
  227. 'Connecting': 'Yhdistetään'
  228. 'Connection re-established': 'Yhteys muodostettu uudelleen'
  229. 'Today': 'Tänään'
  230. 'Send': 'Lähetä'
  231. 'Chat closed by %s': '%s sulki keskustelun'
  232. 'Compose your message...': 'Luo viestisi...'
  233. 'All colleagues are busy.': 'Kaikki kollegat ovat varattuja.'
  234. 'You are on waiting list position <strong>%s</strong>.': 'Olet odotuslistalla sijalla <strong>%s</strong>.'
  235. 'Start new conversation': 'Aloita uusi keskustelu'
  236. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Koska et vastannut viimeiseen %s minuuttiin, keskustelusi <strong>%s</strong> kanssa suljettiin.'
  237. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Koska et vastannut viimeiseen %s minuuttiin, keskustelusi suljettiin.'
  238. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Olemme pahoillamme, tyhjän paikan vapautumisessa kestää odotettua pidempään. Ole hyvä ja yritä myöhemmin uudestaan tai lähetä meille sähköpostia. Kiitos!'
  239. 'fr':
  240. '<strong>Chat</strong> with us!': '<strong>Chattez</strong> avec nous!'
  241. 'Scroll down to see new messages': 'Faites défiler pour lire les nouveaux messages'
  242. 'Online': 'En-ligne'
  243. 'Offline': 'Hors-ligne'
  244. 'Connecting': 'Connexion en cours'
  245. 'Connection re-established': 'Connexion rétablie'
  246. 'Today': 'Aujourdhui'
  247. 'Send': 'Envoyer'
  248. 'Chat closed by %s': 'Chat fermé par %s'
  249. 'Compose your message...': 'Composez votre message...'
  250. 'All colleagues are busy.': 'Tous les collaborateurs sont occupés actuellement.'
  251. 'You are on waiting list position <strong>%s</strong>.': 'Vous êtes actuellement en position <strong>%s</strong> dans la file d\'attente.'
  252. 'Start new conversation': 'Démarrer une nouvelle conversation'
  253. '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 sera fermée.'
  254. '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.'
  255. '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. Nous vous remercions!'
  256. 'he':
  257. '<strong>Chat</strong> with us!': '<strong>שוחח</strong>איתנו!'
  258. 'Scroll down to see new messages': 'גלול מטה כדי לראות הודעות חדשות'
  259. 'Online': 'מחובר'
  260. 'Offline': 'מנותק'
  261. 'Connecting': 'מתחבר'
  262. 'Connection re-established': 'החיבור שוחזר'
  263. 'Today': 'היום'
  264. 'Send': 'שלח'
  265. 'Chat closed by %s': 'הצאט נסגר ע"י %s'
  266. 'Compose your message...': 'כתוב את ההודעה שלך ...'
  267. 'All colleagues are busy.': 'כל הנציגים תפוסים'
  268. 'You are on waiting list position <strong>%s</strong>.': 'מיקומך בתור <strong>%s</strong>.'
  269. 'Start new conversation': 'התחל שיחה חדשה'
  270. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'מכיוון שלא הגבת במהלך %s דקות השיחה שלך עם <strong>%s</strong> נסגרה.'
  271. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'מכיוון שלא הגבת במהלך %s הדקות האחרונות השיחה שלך נסגרה.'
  272. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'מצטערים, הזמן לקבלת נציג ארוך מהרגיל. נסה שוב מאוחר יותר או שלח לנו דוא"ל. תודה!'
  273. 'hu':
  274. '<strong>Chat</strong> with us!': '<strong>Chatelj</strong> velünk!'
  275. 'Scroll down to see new messages': 'Görgess lejjebb az újabb üzenetekért'
  276. 'Online': 'Online'
  277. 'Offline': 'Offline'
  278. 'Connecting': 'Csatlakozás'
  279. 'Connection re-established': 'Újracsatlakozás'
  280. 'Today': 'Ma'
  281. 'Send': 'Küldés'
  282. 'Chat closed by %s': 'A beszélgetést lezárta %s'
  283. 'Compose your message...': 'Írj üzenetet...'
  284. 'All colleagues are busy.': 'Jelenleg minden kollégánk elfoglalt.'
  285. 'You are on waiting list position <strong>%s</strong>.': 'A várólistán a <strong>%s</strong>. pozícióban várakozol.'
  286. 'Start new conversation': 'Új beszélgetés indítása'
  287. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Mivel %s perce nem érkezett újabb üzenet, ezért a <strong>%s</strong> kollégával folytatott beszéletést lezártuk.'
  288. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Mivel %s perce nem érkezett válasz, a beszélgetés lezárult.'
  289. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Sajnáljuk, de a várakozási idő hosszabb a szokásosnál. Kérlek próbáld újra, vagy írd meg kérdésed emailben. Köszönjük!'
  290. 'nl':
  291. '<strong>Chat</strong> with us!': '<strong>Chat</strong> met ons!'
  292. 'Scroll down to see new messages': 'Scrol naar beneden om nieuwe berichten te zien'
  293. 'Online': 'Online'
  294. 'Offline': 'Offline'
  295. 'Connecting': 'Verbinden'
  296. 'Connection re-established': 'Verbinding herstelt'
  297. 'Today': 'Vandaag'
  298. 'Send': 'Verzenden'
  299. 'Chat closed by %s': 'Chat gesloten door %s'
  300. 'Compose your message...': 'Typ uw bericht...'
  301. 'All colleagues are busy.': 'Alle medewerkers zijn bezet.'
  302. 'You are on waiting list position <strong>%s</strong>.': 'U bent <strong>%s</strong> in de wachtrij.'
  303. 'Start new conversation': 'Nieuwe conversatie starten'
  304. '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.'
  305. '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.'
  306. '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!'
  307. 'it':
  308. '<strong>Chat</strong> with us!': '<strong>Chatta</strong> con noi!'
  309. 'Scroll down to see new messages': 'Scorrere verso il basso per vedere i nuovi messaggi'
  310. 'Online': 'Online'
  311. 'Offline': 'Offline'
  312. 'Connecting': 'Collegamento'
  313. 'Connection re-established': 'Collegamento ristabilito'
  314. 'Today': 'Oggi'
  315. 'Send': 'Invio'
  316. 'Chat closed by %s': 'Conversazione chiusa da %s'
  317. 'Compose your message...': 'Comporre il tuo messaggio...'
  318. 'All colleagues are busy.': 'Tutti i colleghi sono occupati.'
  319. 'You are on waiting list position <strong>%s</strong>.': 'Siete in posizione lista d\' attesa <strong>%s</strong>.'
  320. 'Start new conversation': 'Avviare una nuova conversazione'
  321. '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.'
  322. '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.'
  323. '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!'
  324. 'pl':
  325. '<strong>Chat</strong> with us!': '<strong>Czatuj</strong> z nami!'
  326. 'Scroll down to see new messages': 'Przewiń w dół, aby wyświetlić nowe wiadomości'
  327. 'Online': 'Online'
  328. 'Offline': 'Offline'
  329. 'Connecting': 'Łączenie'
  330. 'Connection re-established': 'Ponowne nawiązanie połączenia'
  331. 'Today': 'dzisiejszy'
  332. 'Send': 'Wyślij'
  333. 'Chat closed by %s': 'Czat zamknięty przez %s'
  334. 'Compose your message...': 'Utwórz swoją wiadomość...'
  335. 'All colleagues are busy.': 'Wszyscy koledzy są zajęci.'
  336. 'You are on waiting list position <strong>%s</strong>.': 'Na liście oczekujących znajduje się pozycja <strong>%s</strong>.'
  337. 'Start new conversation': 'Rozpoczęcie nowej konwersacji'
  338. '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.'
  339. '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.'
  340. '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ę!'
  341. 'pt-br': {
  342. '<strong>Chat</strong> with us!': '<strong>Chat</strong> fale conosco!',
  343. 'Scroll down to see new messages': 'Role para baixo, para ver nosvas mensagens',
  344. 'Online': 'Online',
  345. 'Offline': 'Desconectado',
  346. 'Connecting': 'Conectando',
  347. 'Connection re-established': 'Conexão restabelecida',
  348. 'Today': 'Hoje',
  349. 'Send': 'Enviar',
  350. 'Chat closed by %s': 'Chat encerrado por %s',
  351. 'Compose your message...': 'Escreva sua mensagem...',
  352. 'All colleagues are busy.': 'Todos os agentes estão ocupados.',
  353. 'You are on waiting list position <strong>%s</strong>.': 'Você está na posição <strong>%s</strong> na fila de espera.',
  354. 'Start new conversation': 'Iniciar uma nova conversa',
  355. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Como você não respondeu nos últimos %s minutos sua conversa com <strong>%s</strong> foi encerrada.',
  356. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Como você não respondeu nos últimos %s minutos sua conversa foi encerrada.',
  357. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Desculpe, mas o tempo de espera por um agente foi excedido. Tente novamente mais tarde ou nós envie um email. Obrigado'
  358. },
  359. 'zh-cn':
  360. '<strong>Chat</strong> with us!': '发起<strong>即时对话</strong>!'
  361. 'Scroll down to see new messages': '向下滚动以查看新消息'
  362. 'Online': '在线'
  363. 'Offline': '离线'
  364. 'Connecting': '连接中'
  365. 'Connection re-established': '正在重新建立连接'
  366. 'Today': '今天'
  367. 'Send': '发送'
  368. 'Chat closed by %s': 'Chat closed by %s'
  369. 'Compose your message...': '正在输入信息...'
  370. 'All colleagues are busy.': '所有工作人员都在忙碌中.'
  371. 'You are on waiting list position <strong>%s</strong>.': '您目前的等候位置是第 <strong>%s</strong> 位.'
  372. 'Start new conversation': '开始新的会话'
  373. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': '由于您超过 %s 分钟没有回复, 您与 <strong>%s</strong> 的会话已被关闭.'
  374. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': '由于您超过 %s 分钟没有任何回复, 该对话已被关闭.'
  375. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': '非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!'
  376. 'zh-tw':
  377. '<strong>Chat</strong> with us!': '開始<strong>即時對话</strong>!'
  378. 'Scroll down to see new messages': '向下滑動以查看新訊息'
  379. 'Online': '線上'
  380. 'Offline': '离线'
  381. 'Connecting': '連線中'
  382. 'Connection re-established': '正在重新建立連線中'
  383. 'Today': '今天'
  384. 'Send': '發送'
  385. 'Chat closed by %s': 'Chat closed by %s'
  386. 'Compose your message...': '正在輸入訊息...'
  387. 'All colleagues are busy.': '所有服務人員都在忙碌中.'
  388. 'You are on waiting list position <strong>%s</strong>.': '你目前的等候位置是第 <strong>%s</strong> 順位.'
  389. 'Start new conversation': '開始新的對話'
  390. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': '由於你超過 %s 分鐘沒有回應, 你與 <strong>%s</strong> 的對話已被關閉.'
  391. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': '由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.'
  392. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': '非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!'
  393. 'ru':
  394. '<strong>Chat</strong> with us!': 'Напишите нам!'
  395. 'Scroll down to see new messages': 'Прокрутите, чтобы увидеть новые сообщения'
  396. 'Online': 'Онлайн'
  397. 'Offline': 'Оффлайн'
  398. 'Connecting': 'Подключение'
  399. 'Connection re-established': 'Подключение восстановлено'
  400. 'Today': 'Сегодня'
  401. 'Send': 'Отправить'
  402. 'Chat closed by %s': '%s закрыл чат'
  403. 'Compose your message...': 'Напишите сообщение...'
  404. 'All colleagues are busy.': 'Все сотрудники заняты'
  405. 'You are on waiting list position %s.': 'Вы в списке ожидания под номером %s'
  406. 'Start new conversation': 'Начать новую переписку.'
  407. 'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.'
  408. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.'
  409. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!'
  410. 'sv':
  411. '<strong>Chat</strong> with us!': '<strong>Chatta</strong> med oss!'
  412. 'Scroll down to see new messages': 'Rulla ner för att se nya meddelanden'
  413. 'Online': 'Online'
  414. 'Offline': 'Offline'
  415. 'Connecting': 'Ansluter'
  416. 'Connection re-established': 'Anslutningen återupprättas'
  417. 'Today': 'I dag'
  418. 'Send': 'Skicka'
  419. 'Chat closed by %s': 'Chatt stängd av %s'
  420. 'Compose your message...': 'Skriv ditt meddelande...'
  421. 'All colleagues are busy.': 'Alla kollegor är upptagna.'
  422. 'You are on waiting list position <strong>%s</strong>.': 'Du är på väntelistan som position <strong>%s</strong>.'
  423. 'Start new conversation': 'Starta ny konversation'
  424. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Eftersom du inte svarat inom %s minuterna i din konversation med <strong>%s</strong> så stängdes chatten.'
  425. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Då du inte svarat inom de senaste %s minuterna så avslutades din chatt.'
  426. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Vi är ledsna, det tar längre tid som förväntat att få en ledig plats. Försök igen senare eller skicka ett e-postmeddelande till oss. Tack!'
  427. 'no':
  428. '<strong>Chat</strong> with us!': '<strong>Chat</strong> med oss!'
  429. 'Scroll down to see new messages': 'Bla ned for å se nye meldinger'
  430. 'Online': 'Pålogget'
  431. 'Offline': 'Avlogget'
  432. 'Connecting': 'Koble til'
  433. 'Connection re-established': 'Tilkoblingen er gjenopprettet'
  434. 'Today': 'I dag'
  435. 'Send': 'Send'
  436. 'Chat closed by %s': 'Chat avsluttes om %s'
  437. 'Compose your message...': 'Skriv din melding...'
  438. 'All colleagues are busy.': 'Alle våre kolleger er for øyeblikket opptatt.'
  439. 'You are on waiting list position <strong>%s</strong>.': 'Du står nå i kø og er nr. <strong>%s</strong> på ventelisten.'
  440. 'Start new conversation': 'Start en ny samtale'
  441. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Ettersom du ikke har respondert i løpet av de siste %s minuttene av samtalen, vil samtalen med <strong>%s</strong> nå avsluttes.'
  442. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Ettersom du ikke har respondert i løpet av de siste %s minuttene, har samtalen nå blitt avsluttet.'
  443. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Vi beklager, men det tar lengre tid enn vanlig å få en ledig plass i vår chat. Vennligst prøv igjen på et senere tidspunkt eller send oss en e-post. Tusen takk!'
  444. 'nb':
  445. '<strong>Chat</strong> with us!': '<strong>Chat</strong> med oss!'
  446. 'Scroll down to see new messages': 'Bla ned for å se nye meldinger'
  447. 'Online': 'Pålogget'
  448. 'Offline': 'Avlogget'
  449. 'Connecting': 'Koble til'
  450. 'Connection re-established': 'Tilkoblingen er gjenopprettet'
  451. 'Today': 'I dag'
  452. 'Send': 'Send'
  453. 'Chat closed by %s': 'Chat avsluttes om %s'
  454. 'Compose your message...': 'Skriv din melding...'
  455. 'All colleagues are busy.': 'Alle våre kolleger er for øyeblikket opptatt.'
  456. 'You are on waiting list position <strong>%s</strong>.': 'Du står nå i kø og er nr. <strong>%s</strong> på ventelisten.'
  457. 'Start new conversation': 'Start en ny samtale'
  458. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Ettersom du ikke har respondert i løpet av de siste %s minuttene av samtalen, vil samtalen med <strong>%s</strong> nå avsluttes.'
  459. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Ettersom du ikke har respondert i løpet av de siste %s minuttene, har samtalen nå blitt avsluttet.'
  460. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Vi beklager, men det tar lengre tid enn vanlig å få en ledig plass i vår chat. Vennligst prøv igjen på et senere tidspunkt eller send oss en e-post. Tusen takk!'
  461. 'el':
  462. '<strong>Chat</strong> with us!': '<strong>Επικοινωνήστε</strong> μαζί μας!'
  463. 'Scroll down to see new messages': 'Μεταβείτε κάτω για να δείτε τα νέα μηνύματα'
  464. 'Online': 'Σε σύνδεση'
  465. 'Offline': 'Αποσυνδεμένος'
  466. 'Connecting': 'Σύνδεση'
  467. 'Connection re-established': 'Η σύνδεση αποκαταστάθηκε'
  468. 'Today': 'Σήμερα'
  469. 'Send': 'Αποστολή'
  470. 'Chat closed by %s': 'Η συνομιλία έκλεισε από τον/την %s'
  471. 'Compose your message...': 'Γράψτε το μήνυμα σας...'
  472. 'All colleagues are busy.': 'Όλοι οι συνάδελφοι μας είναι απασχολημένοι.'
  473. 'You are on waiting list position <strong>%s</strong>.': 'Βρίσκεστε σε λίστα αναμονής στη θέση <strong>%s</strong>.'
  474. 'Start new conversation': 'Έναρξη νέας συνομιλίας'
  475. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Από τη στιγμή που δεν απαντήσατε τα τελευταία %s λεπτά η συνομιλία σας με τον/την <strong>%s</strong> έκλεισε.'
  476. 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Από τη στιγμή που δεν απαντήσατε τα τελευταία %s λεπτά η συνομιλία σας έκλεισε.'
  477. 'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Λυπούμαστε που χρειάζεται περισσότερος χρόνος από τον αναμενόμενο για να βρεθεί μία κενή θέση. Παρακαλούμε δοκιμάστε ξανά αργότερα ή στείλτε μας ένα email. Ευχαριστούμε!'
  478. sessionId: undefined
  479. scrolledToBottom: true
  480. scrollSnapTolerance: 10
  481. richTextFormatKey:
  482. 66: true # b
  483. 73: true # i
  484. 85: true # u
  485. 83: true # s
  486. T: (string, items...) =>
  487. if @options.lang && @options.lang isnt 'en'
  488. if !@translations[@options.lang]
  489. @log.notice "Translation '#{@options.lang}' needed!"
  490. else
  491. translations = @translations[@options.lang]
  492. if !translations[string]
  493. @log.notice "Translation needed for '#{string}'"
  494. string = translations[string] || string
  495. if items
  496. for item in items
  497. string = string.replace(/%s/, item)
  498. string
  499. view: (name) =>
  500. return (options) =>
  501. if !options
  502. options = {}
  503. options.T = @T
  504. options.background = @options.background
  505. options.flat = @options.flat
  506. options.fontSize = @options.fontSize
  507. return window.zammadChatTemplates[name](options)
  508. constructor: (options) ->
  509. super(options)
  510. # jQuery migration
  511. if typeof jQuery != 'undefined' && @options.target instanceof jQuery
  512. @log.notice 'Chat: target option is a jQuery object. jQuery is not a requirement for the chat any more.'
  513. @options.target = @options.target.get(0)
  514. # fullscreen
  515. @isFullscreen = (window.matchMedia and window.matchMedia('(max-width: 768px)').matches)
  516. @scrollRoot = @getScrollRoot()
  517. # check prerequisites
  518. if !window.WebSocket or !sessionStorage
  519. @state = 'unsupported'
  520. @log.notice 'Chat: Browser not supported!'
  521. return
  522. if !@options.chatId
  523. @state = 'unsupported'
  524. @log.error 'Chat: need chatId as option!'
  525. return
  526. # detect language
  527. if !@options.lang
  528. @options.lang = document.documentElement.getAttribute('lang')
  529. if @options.lang
  530. if !@translations[@options.lang]
  531. @log.debug "lang: No #{@options.lang} found, try first two letters"
  532. @options.lang = @options.lang.replace(/-.+?$/, '') # replace "-xx" of xx-xx
  533. @log.debug "lang: #{@options.lang}"
  534. # detect host
  535. @detectHost() if !@options.host
  536. @loadCss()
  537. @io = new Io(@options)
  538. @io.set(
  539. onOpen: @render
  540. onClose: @onWebSocketClose
  541. onMessage: @onWebSocketMessage
  542. onError: @onError
  543. )
  544. @io.connect()
  545. getScrollRoot: ->
  546. return document.scrollingElement if 'scrollingElement' of document
  547. html = document.documentElement
  548. start = parseInt(html.pageYOffset, 10)
  549. html.pageYOffset = start + 1
  550. end = parseInt(html.pageYOffset, 10)
  551. html.pageYOffset = start
  552. return if end > start then html else document.body
  553. render: =>
  554. if !@el || !document.querySelector('.zammad-chat')
  555. @renderBase()
  556. # disable open button
  557. btn = document.querySelector(".#{ @options.buttonClass }")
  558. if btn
  559. btn.classList.add @options.inactiveClass
  560. @setAgentOnlineState 'online'
  561. @log.debug 'widget rendered'
  562. @startTimeoutObservers()
  563. @idleTimeout.start()
  564. # get current chat status
  565. @sessionId = sessionStorage.getItem('sessionId')
  566. @send 'chat_status_customer',
  567. session_id: @sessionId
  568. url: window.location.href
  569. renderBase: ->
  570. @el.remove() if @el
  571. @options.target.insertAdjacentHTML('beforeend', @view('chat')(
  572. title: @options.title,
  573. scrollHint: @options.scrollHint
  574. ))
  575. @el = @options.target.querySelector('.zammad-chat')
  576. @input = @el.querySelector('.zammad-chat-input')
  577. @body = @el.querySelector('.zammad-chat-body')
  578. # start bindings
  579. @el.querySelector('.js-chat-open').addEventListener('click', @open)
  580. @el.querySelector('.js-chat-toggle').addEventListener('click', @toggle)
  581. @el.querySelector('.js-chat-status').addEventListener('click', @stopPropagation)
  582. @el.querySelector('.zammad-chat-controls').addEventListener('submit', @onSubmit)
  583. @body.addEventListener('scroll', @detectScrolledtoBottom)
  584. @el.querySelector('.zammad-scroll-hint').addEventListener('click', @onScrollHintClick)
  585. @input.addEventListener('keydown', @onKeydown)
  586. @input.addEventListener('input', @onInput)
  587. @input.addEventListener('paste', @onPaste)
  588. @input.addEventListener('drop', @onDrop)
  589. window.addEventListener('beforeunload', @onLeaveTemporary)
  590. window.addEventListener('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. stopPropagation: (event) ->
  600. event.stopPropagation()
  601. onDrop: (e) =>
  602. e.stopPropagation()
  603. e.preventDefault()
  604. if window.dataTransfer # ie
  605. dataTransfer = window.dataTransfer
  606. else if e.dataTransfer # other browsers
  607. dataTransfer = e.dataTransfer
  608. else
  609. throw 'No clipboardData support'
  610. x = e.clientX
  611. y = e.clientY
  612. file = dataTransfer.files[0]
  613. # look for images
  614. if file.type.match('image.*')
  615. reader = new FileReader()
  616. reader.onload = (e) =>
  617. # Insert the image at the carat
  618. insert = (dataUrl, width) =>
  619. # adapt image if we are on retina devices
  620. if @isRetina()
  621. width = width / 2
  622. result = dataUrl
  623. img = new Image()
  624. img.style.width = '100%'
  625. img.style.maxWidth = width + 'px'
  626. img.src = result
  627. if document.caretPositionFromPoint
  628. pos = document.caretPositionFromPoint(x, y)
  629. range = document.createRange()
  630. range.setStart(pos.offsetNode, pos.offset)
  631. range.collapse()
  632. range.insertNode(img)
  633. else if document.caretRangeFromPoint
  634. range = document.caretRangeFromPoint(x, y)
  635. range.insertNode(img)
  636. else
  637. console.log('could not find carat')
  638. # resize if to big
  639. @resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
  640. reader.readAsDataURL(file)
  641. onPaste: (e) =>
  642. e.stopPropagation()
  643. e.preventDefault()
  644. if e.clipboardData
  645. clipboardData = e.clipboardData
  646. else if window.clipboardData
  647. clipboardData = window.clipboardData
  648. else if e.clipboardData
  649. clipboardData = e.clipboardData
  650. else
  651. throw 'No clipboardData support'
  652. imageInserted = false
  653. if clipboardData && clipboardData.items && clipboardData.items[0]
  654. item = clipboardData.items[0]
  655. if item.kind == 'file' && (item.type == 'image/png' || item.type == 'image/jpeg')
  656. imageFile = item.getAsFile()
  657. reader = new FileReader()
  658. reader.onload = (e) =>
  659. insert = (dataUrl, width) =>
  660. # adapt image if we are on retina devices
  661. if @isRetina()
  662. width = width / 2
  663. img = new Image()
  664. img.style.width = '100%'
  665. img.style.maxWidth = width + 'px'
  666. img.src = dataUrl
  667. document.execCommand('insertHTML', false, img)
  668. # resize if to big
  669. @resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
  670. reader.readAsDataURL(imageFile)
  671. imageInserted = true
  672. return if imageInserted
  673. # check existing + paste text for limit
  674. text = undefined
  675. docType = undefined
  676. try
  677. text = clipboardData.getData('text/html')
  678. docType = 'html'
  679. if !text || text.length is 0
  680. docType = 'text'
  681. text = clipboardData.getData('text/plain')
  682. if !text || text.length is 0
  683. docType = 'text2'
  684. text = clipboardData.getData('text')
  685. catch e
  686. console.log('Sorry, can\'t insert markup because browser is not supporting it.')
  687. docType = 'text3'
  688. text = clipboardData.getData('text')
  689. if docType is 'text' || docType is 'text2' || docType is 'text3'
  690. text = '<div>' + text.replace(/\n/g, '</div><div>') + '</div>'
  691. text = text.replace(/<div><\/div>/g, '<div><br></div>')
  692. console.log('p', docType, text)
  693. if docType is 'html'
  694. html = document.createElement('div')
  695. html.innerHTML = text
  696. match = false
  697. htmlTmp = text
  698. regex = new RegExp('<(/w|w)\:[A-Za-z]')
  699. if htmlTmp.match(regex)
  700. match = true
  701. htmlTmp = htmlTmp.replace(regex, '')
  702. regex = new RegExp('<(/o|o)\:[A-Za-z]')
  703. if htmlTmp.match(regex)
  704. match = true
  705. htmlTmp = htmlTmp.replace(regex, '')
  706. if match
  707. html = @wordFilter(html)
  708. #html
  709. for node in html.childNodes
  710. if node.nodeType == 8
  711. node.remove()
  712. # remove tags, keep content
  713. for node in html.querySelectorAll('a, font, small, time, form, label')
  714. node.outerHTML = node.innerHTML
  715. # replace tags with generic div
  716. # New type of the tag
  717. replacementTag = 'div';
  718. # Replace all x tags with the type of replacementTag
  719. for node in html.querySelectorAll('textarea')
  720. outer = node.outerHTML
  721. # Replace opening tag
  722. regex = new RegExp('<' + node.tagName, 'i')
  723. newTag = outer.replace(regex, '<' + replacementTag)
  724. # Replace closing tag
  725. regex = new RegExp('</' + node.tagName, 'i')
  726. newTag = newTag.replace(regex, '</' + replacementTag)
  727. node.outerHTML = newTag
  728. # remove tags & content
  729. for node in html.querySelectorAll('font, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset')
  730. node.remove()
  731. @removeAttributes(html)
  732. text = html.innerHTML
  733. # as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower)
  734. if docType is 'text3'
  735. @pasteHtmlAtCaret(text)
  736. else
  737. document.execCommand('insertHTML', false, text)
  738. true
  739. onKeydown: (e) =>
  740. # check for enter
  741. if not @inputDisabled and not e.shiftKey and e.keyCode is 13
  742. e.preventDefault()
  743. @sendMessage()
  744. richtTextControl = false
  745. if !e.altKey && !e.ctrlKey && e.metaKey
  746. richtTextControl = true
  747. else if !e.altKey && e.ctrlKey && !e.metaKey
  748. richtTextControl = true
  749. if richtTextControl && @richTextFormatKey[ e.keyCode ]
  750. e.preventDefault()
  751. if e.keyCode is 66
  752. document.execCommand('bold')
  753. return true
  754. if e.keyCode is 73
  755. document.execCommand('italic')
  756. return true
  757. if e.keyCode is 85
  758. document.execCommand('underline')
  759. return true
  760. if e.keyCode is 83
  761. document.execCommand('strikeThrough')
  762. return true
  763. send: (event, data = {}) =>
  764. data.chat_id = @options.chatId
  765. @io.send(event, data)
  766. onWebSocketMessage: (pipes) =>
  767. for pipe in pipes
  768. @log.debug 'ws:onmessage', pipe
  769. switch pipe.event
  770. when 'chat_error'
  771. @log.notice pipe.data
  772. if pipe.data && pipe.data.state is 'chat_disabled'
  773. @destroy(remove: true)
  774. when 'chat_session_message'
  775. return if pipe.data.self_written
  776. @receiveMessage pipe.data
  777. when 'chat_session_typing'
  778. return if pipe.data.self_written
  779. @onAgentTypingStart()
  780. when 'chat_session_start'
  781. @onConnectionEstablished pipe.data
  782. when 'chat_session_queue'
  783. @onQueueScreen pipe.data
  784. when 'chat_session_closed'
  785. @onSessionClosed pipe.data
  786. when 'chat_session_left'
  787. @onSessionClosed pipe.data
  788. when 'chat_status_customer'
  789. switch pipe.data.state
  790. when 'online'
  791. @sessionId = undefined
  792. if !@options.cssAutoload || @cssLoaded
  793. @onReady()
  794. else
  795. @socketReady = true
  796. when 'offline'
  797. @onError 'Zammad Chat: No agent online'
  798. when 'chat_disabled'
  799. @onError 'Zammad Chat: Chat is disabled'
  800. when 'no_seats_available'
  801. @onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}"
  802. when 'reconnect'
  803. @onReopenSession pipe.data
  804. onReady: ->
  805. @log.debug 'widget ready for use'
  806. btn = document.querySelector(".#{ @options.buttonClass }")
  807. if btn
  808. btn.addEventListener('click', @open)
  809. btn.classList.remove(@options.inactiveClass)
  810. @options.onReady?()
  811. if @options.show
  812. @show()
  813. onError: (message) =>
  814. @log.debug message
  815. @addStatus(message)
  816. btn = document.querySelector(".#{ @options.buttonClass }")
  817. if btn
  818. btn.classList.add('zammad-chat-is-hidden')
  819. if @isOpen
  820. @disableInput()
  821. @destroy(remove: false)
  822. else
  823. @destroy(remove: true)
  824. @options.onError?(message)
  825. onReopenSession: (data) =>
  826. @log.debug 'old messages', data.session
  827. @inactiveTimeout.start()
  828. unfinishedMessage = sessionStorage.getItem 'unfinished_message'
  829. # rerender chat history
  830. if data.agent
  831. @onConnectionEstablished(data)
  832. for message in data.session
  833. @renderMessage
  834. message: message.content
  835. id: message.id
  836. from: if message.created_by_id then 'agent' else 'customer'
  837. if unfinishedMessage
  838. @input.innerHTML = unfinishedMessage
  839. # show wait list
  840. if data.position
  841. @onQueue data
  842. @show()
  843. @open()
  844. @scrollToBottom()
  845. if unfinishedMessage
  846. @input.focus()
  847. onInput: =>
  848. # remove unread-state from messages
  849. for message in @el.querySelectorAll('.zammad-chat-message--unread')
  850. message.classList.remove 'zammad-chat-message--unread'
  851. sessionStorage.setItem 'unfinished_message', @input.innerHTML
  852. @onTyping()
  853. onTyping: ->
  854. # send typing start event only every 1.5 seconds
  855. return if @isTyping && @isTyping > new Date(new Date().getTime() - 1500)
  856. @isTyping = new Date()
  857. @send 'chat_session_typing',
  858. session_id: @sessionId
  859. @inactiveTimeout.start()
  860. onSubmit: (event) =>
  861. event.preventDefault()
  862. @sendMessage()
  863. sendMessage: ->
  864. message = @input.innerHTML
  865. return if !message
  866. @inactiveTimeout.start()
  867. sessionStorage.removeItem 'unfinished_message'
  868. messageElement = @view('message')
  869. message: message
  870. from: 'customer'
  871. id: @_messageCount++
  872. unreadClass: ''
  873. @maybeAddTimestamp()
  874. # add message before message typing loader
  875. if @el.querySelector('.zammad-chat-message--typing')
  876. @lastAddedType = 'typing-placeholder'
  877. @el.querySelector('.zammad-chat-message--typing').insertAdjacentHTML('beforebegin', messageElement)
  878. else
  879. @lastAddedType = 'message--customer'
  880. @body.insertAdjacentHTML('beforeend', messageElement)
  881. @input.innerHTML = ''
  882. @scrollToBottom()
  883. # send message event
  884. @send 'chat_session_message',
  885. content: message
  886. id: @_messageCount
  887. session_id: @sessionId
  888. receiveMessage: (data) =>
  889. @inactiveTimeout.start()
  890. # hide writing indicator
  891. @onAgentTypingEnd()
  892. @maybeAddTimestamp()
  893. @renderMessage
  894. message: data.message.content
  895. id: data.id
  896. from: 'agent'
  897. @scrollToBottom showHint: true
  898. renderMessage: (data) =>
  899. @lastAddedType = "message--#{ data.from }"
  900. data.unreadClass = if document.hidden then ' zammad-chat-message--unread' else ''
  901. @body.insertAdjacentHTML('beforeend', @view('message')(data))
  902. open: =>
  903. if @isOpen
  904. @log.debug 'widget already open, block'
  905. return
  906. @isOpen = true
  907. @log.debug 'open widget'
  908. @show()
  909. if !@sessionId
  910. @showLoader()
  911. @el.classList.add 'zammad-chat-is-open'
  912. remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight
  913. @el.style.transform = "translateY(#{remainerHeight}px)"
  914. # force redraw
  915. @el.clientHeight
  916. if !@sessionId
  917. @el.addEventListener 'transitionend', @onOpenAnimationEnd
  918. @el.classList.add 'zammad-chat--animate'
  919. # force redraw
  920. @el.clientHeight
  921. # start animation
  922. @el.style.transform = ''
  923. @send('chat_session_init'
  924. url: window.location.href
  925. )
  926. else
  927. @el.style.transform = ''
  928. @onOpenAnimationEnd()
  929. onOpenAnimationEnd: =>
  930. @el.removeEventListener 'transitionend', @onOpenAnimationEnd
  931. @el.classList.remove 'zammad-chat--animate'
  932. @idleTimeout.stop()
  933. if @isFullscreen
  934. @disableScrollOnRoot()
  935. @options.onOpenAnimationEnd?()
  936. sessionClose: =>
  937. # send close
  938. @send 'chat_session_close',
  939. session_id: @sessionId
  940. # stop timer
  941. @inactiveTimeout.stop()
  942. @waitingListTimeout.stop()
  943. # delete input store
  944. sessionStorage.removeItem 'unfinished_message'
  945. # stop delay of initial queue position
  946. if @onInitialQueueDelayId
  947. clearTimeout(@onInitialQueueDelayId)
  948. @setSessionId undefined
  949. toggle: (event) =>
  950. if @isOpen
  951. @close(event)
  952. else
  953. @open(event)
  954. close: (event) =>
  955. if !@isOpen
  956. @log.debug 'can\'t close widget, it\'s not open'
  957. return
  958. if @initDelayId
  959. clearTimeout(@initDelayId)
  960. if !@sessionId
  961. @log.debug 'can\'t close widget without sessionId'
  962. return
  963. @log.debug 'close widget'
  964. event.stopPropagation() if event
  965. @sessionClose()
  966. if @isFullscreen
  967. @enableScrollOnRoot()
  968. # close window
  969. remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight
  970. @el.addEventListener 'transitionend', @onCloseAnimationEnd
  971. @el.classList.add 'zammad-chat--animate'
  972. # force redraw
  973. document.offsetHeight
  974. # animate out
  975. @el.style.transform = "translateY(#{remainerHeight}px)"
  976. onCloseAnimationEnd: =>
  977. @el.removeEventListener 'transitionend', @onCloseAnimationEnd
  978. @el.classList.remove 'zammad-chat-is-open', 'zammad-chat--animate'
  979. @el.style.transform = ''
  980. @showLoader()
  981. @el.querySelector('.zammad-chat-welcome').classList.remove('zammad-chat-is-hidden')
  982. @el.querySelector('.zammad-chat-agent').classList.add('zammad-chat-is-hidden')
  983. @el.querySelector('.zammad-chat-agent-status').classList.add('zammad-chat-is-hidden')
  984. @isOpen = false
  985. @options.onCloseAnimationEnd?()
  986. @io.reconnect()
  987. onWebSocketClose: =>
  988. return if @isOpen
  989. if @el
  990. @el.classList.remove('zammad-chat-is-shown')
  991. @el.classList.remove('zammad-chat-is-loaded')
  992. show: ->
  993. return if @state is 'offline'
  994. @el.classList.add('zammad-chat-is-loaded')
  995. @el.classList.add('zammad-chat-is-shown')
  996. disableInput: ->
  997. @inputDisabled = true
  998. @input.setAttribute('contenteditable', false)
  999. @el.querySelector('.zammad-chat-send').disabled = true
  1000. @io.close()
  1001. enableInput: ->
  1002. @inputDisabled = false
  1003. @input.setAttribute('contenteditable', true)
  1004. @el.querySelector('.zammad-chat-send').disabled = false
  1005. hideModal: ->
  1006. @el.querySelector('.zammad-chat-modal').innerHTML = ''
  1007. onQueueScreen: (data) =>
  1008. @setSessionId data.session_id
  1009. # delay initial queue position, show connecting first
  1010. show = =>
  1011. @onQueue data
  1012. @waitingListTimeout.start()
  1013. if @initialQueueDelay && !@onInitialQueueDelayId
  1014. @onInitialQueueDelayId = setTimeout(show, @initialQueueDelay)
  1015. return
  1016. # stop delay of initial queue position
  1017. if @onInitialQueueDelayId
  1018. clearTimeout(@onInitialQueueDelayId)
  1019. # show queue position
  1020. show()
  1021. onQueue: (data) =>
  1022. @log.notice 'onQueue', data.position
  1023. @inQueue = true
  1024. @el.querySelector('.zammad-chat-modal').innerHTML = @view('waiting')
  1025. position: data.position
  1026. onAgentTypingStart: =>
  1027. if @stopTypingId
  1028. clearTimeout(@stopTypingId)
  1029. @stopTypingId = setTimeout(@onAgentTypingEnd, 3000)
  1030. # never display two typing indicators
  1031. return if @el.querySelector('.zammad-chat-message--typing')
  1032. @maybeAddTimestamp()
  1033. @body.insertAdjacentHTML('beforeend', @view('typingIndicator')())
  1034. # only if typing indicator is shown
  1035. return if !@isVisible(@el.querySelector('.zammad-chat-message--typing'), true)
  1036. @scrollToBottom()
  1037. onAgentTypingEnd: =>
  1038. @el.querySelector('.zammad-chat-message--typing').remove() if @el.querySelector('.zammad-chat-message--typing')
  1039. onLeaveTemporary: =>
  1040. return if !@sessionId
  1041. @send 'chat_session_leave_temporary',
  1042. session_id: @sessionId
  1043. maybeAddTimestamp: ->
  1044. timestamp = Date.now()
  1045. if !@lastTimestamp or (timestamp - @lastTimestamp) > @showTimeEveryXMinutes * 60000
  1046. label = @T('Today')
  1047. time = new Date().toTimeString().substr 0,5
  1048. if @lastAddedType is 'timestamp'
  1049. # update last time
  1050. @updateLastTimestamp label, time
  1051. @lastTimestamp = timestamp
  1052. else
  1053. # add new timestamp
  1054. @body.insertAdjacentHTML 'beforeend', @view('timestamp')
  1055. label: label
  1056. time: time
  1057. @lastTimestamp = timestamp
  1058. @lastAddedType = 'timestamp'
  1059. @scrollToBottom()
  1060. updateLastTimestamp: (label, time) ->
  1061. return if !@el
  1062. timestamps = @el.querySelectorAll('.zammad-chat-body .zammad-chat-timestamp')
  1063. return if !timestamps
  1064. timestamps[timestamps.length - 1].outerHTML = @view('timestamp')
  1065. label: label
  1066. time: time
  1067. addStatus: (status) ->
  1068. return if !@el
  1069. @maybeAddTimestamp()
  1070. @body.insertAdjacentHTML 'beforeend', @view('status')
  1071. status: status
  1072. @scrollToBottom()
  1073. detectScrolledtoBottom: =>
  1074. scrollBottom = @body.scrollTop + @body.offsetHeight
  1075. @scrolledToBottom = Math.abs(scrollBottom - @body.scrollHeight) <= @scrollSnapTolerance
  1076. @el.querySelector('.zammad-scroll-hint').classList.add('is-hidden') if @scrolledToBottom
  1077. showScrollHint: ->
  1078. @el.querySelector('.zammad-scroll-hint').classList.remove('is-hidden')
  1079. # compensate scroll
  1080. @body.scrollTop = @body.scrollTop + @el.querySelector('.zammad-scroll-hint').offsetHeight
  1081. onScrollHintClick: =>
  1082. # animate scroll
  1083. @body.scrollTo
  1084. top: @body.scrollHeight
  1085. behavior: 'smooth'
  1086. scrollToBottom: ({ showHint } = { showHint: false }) ->
  1087. if @scrolledToBottom
  1088. @body.scrollTop = @body.scrollHeight
  1089. else if showHint
  1090. @showScrollHint()
  1091. destroy: (params = {}) =>
  1092. @log.debug 'destroy widget', params
  1093. @setAgentOnlineState 'offline'
  1094. if params.remove && @el
  1095. @el.remove()
  1096. # stop all timer
  1097. if @waitingListTimeout
  1098. @waitingListTimeout.stop()
  1099. if @inactiveTimeout
  1100. @inactiveTimeout.stop()
  1101. if @idleTimeout
  1102. @idleTimeout.stop()
  1103. # stop ws connection
  1104. @io.close()
  1105. reconnect: =>
  1106. # set status to connecting
  1107. @log.notice 'reconnecting'
  1108. @disableInput()
  1109. @lastAddedType = 'status'
  1110. @setAgentOnlineState 'connecting'
  1111. @addStatus @T('Connection lost')
  1112. onConnectionReestablished: =>
  1113. # set status back to online
  1114. @lastAddedType = 'status'
  1115. @setAgentOnlineState 'online'
  1116. @addStatus @T('Connection re-established')
  1117. @options.onConnectionReestablished?()
  1118. onSessionClosed: (data) ->
  1119. @addStatus @T('Chat closed by %s', data.realname)
  1120. @disableInput()
  1121. @setAgentOnlineState 'offline'
  1122. @inactiveTimeout.stop()
  1123. @options.onSessionClosed?(data)
  1124. setSessionId: (id) =>
  1125. @sessionId = id
  1126. if id is undefined
  1127. sessionStorage.removeItem 'sessionId'
  1128. else
  1129. sessionStorage.setItem 'sessionId', id
  1130. onConnectionEstablished: (data) =>
  1131. # stop delay of initial queue position
  1132. if @onInitialQueueDelayId
  1133. clearTimeout @onInitialQueueDelayId
  1134. @inQueue = false
  1135. if data.agent
  1136. @agent = data.agent
  1137. if data.session_id
  1138. @setSessionId data.session_id
  1139. # empty old messages
  1140. @body.innerHTML = ''
  1141. @el.querySelector('.zammad-chat-agent').innerHTML = @view('agent')
  1142. agent: @agent
  1143. @enableInput()
  1144. @hideModal()
  1145. @el.querySelector('.zammad-chat-welcome').classList.add('zammad-chat-is-hidden')
  1146. @el.querySelector('.zammad-chat-agent').classList.remove('zammad-chat-is-hidden')
  1147. @el.querySelector('.zammad-chat-agent-status').classList.remove('zammad-chat-is-hidden')
  1148. @input.focus() if not @isFullscreen
  1149. @setAgentOnlineState 'online'
  1150. @waitingListTimeout.stop()
  1151. @idleTimeout.stop()
  1152. @inactiveTimeout.start()
  1153. @options.onConnectionEstablished?(data)
  1154. showCustomerTimeout: ->
  1155. @el.querySelector('.zammad-chat-modal').innerHTML = @view('customer_timeout')
  1156. agent: @agent.name
  1157. delay: @options.inactiveTimeout
  1158. @el.querySelector('.js-restart').addEventListener 'click', -> location.reload()
  1159. @sessionClose()
  1160. showWaitingListTimeout: ->
  1161. @el.querySelector('.zammad-chat-modal').innerHTML = @view('waiting_list_timeout')
  1162. delay: @options.watingListTimeout
  1163. @el.querySelector('.js-restart').addEventListener 'click', -> location.reload()
  1164. @sessionClose()
  1165. showLoader: ->
  1166. @el.querySelector('.zammad-chat-modal').innerHTML = @view('loader')()
  1167. setAgentOnlineState: (state) =>
  1168. @state = state
  1169. return if !@el
  1170. capitalizedState = state.charAt(0).toUpperCase() + state.slice(1)
  1171. @el.querySelector('.zammad-chat-agent-status').dataset.status = state
  1172. @el.querySelector('.zammad-chat-agent-status').textContent = @T(capitalizedState)
  1173. detectHost: ->
  1174. protocol = 'ws://'
  1175. if scriptProtocol is 'https'
  1176. protocol = 'wss://'
  1177. @options.host = "#{ protocol }#{ scriptHost }/ws"
  1178. loadCss: ->
  1179. return if !@options.cssAutoload
  1180. url = @options.cssUrl
  1181. if !url
  1182. url = @options.host
  1183. .replace(/^wss/i, 'https')
  1184. .replace(/^ws/i, 'http')
  1185. .replace(/\/ws$/i, '') # WebSocket may run on example.com/ws path
  1186. url += '/assets/chat/chat.css'
  1187. @log.debug "load css from '#{url}'"
  1188. styles = "@import url('#{url}');"
  1189. newSS = document.createElement('link')
  1190. newSS.onload = @onCssLoaded
  1191. newSS.rel = 'stylesheet'
  1192. newSS.href = 'data:text/css,' + escape(styles)
  1193. document.getElementsByTagName('head')[0].appendChild(newSS)
  1194. onCssLoaded: =>
  1195. @cssLoaded = true
  1196. if @socketReady
  1197. @onReady()
  1198. @options.onCssLoaded?()
  1199. startTimeoutObservers: =>
  1200. @idleTimeout = new Timeout(
  1201. logPrefix: 'idleTimeout'
  1202. debug: @options.debug
  1203. timeout: @options.idleTimeout
  1204. timeoutIntervallCheck: @options.idleTimeoutIntervallCheck
  1205. callback: =>
  1206. @log.debug 'Idle timeout reached, hide widget', new Date
  1207. @destroy(remove: true)
  1208. )
  1209. @inactiveTimeout = new Timeout(
  1210. logPrefix: 'inactiveTimeout'
  1211. debug: @options.debug
  1212. timeout: @options.inactiveTimeout
  1213. timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck
  1214. callback: =>
  1215. @log.debug 'Inactive timeout reached, show timeout screen.', new Date
  1216. @showCustomerTimeout()
  1217. @destroy(remove: false)
  1218. )
  1219. @waitingListTimeout = new Timeout(
  1220. logPrefix: 'waitingListTimeout'
  1221. debug: @options.debug
  1222. timeout: @options.waitingListTimeout
  1223. timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck
  1224. callback: =>
  1225. @log.debug 'Waiting list timeout reached, show timeout screen.', new Date
  1226. @showWaitingListTimeout()
  1227. @destroy(remove: false)
  1228. )
  1229. disableScrollOnRoot: ->
  1230. @rootScrollOffset = @scrollRoot.scrollTop
  1231. @scrollRoot.style.overflow = 'hidden'
  1232. @scrollRoot.style.position = 'fixed'
  1233. enableScrollOnRoot: ->
  1234. @scrollRoot.scrollTop = @rootScrollOffset
  1235. @scrollRoot.style.overflow = ''
  1236. @scrollRoot.style.position = ''
  1237. # based on https://github.com/customd/jquery-visible/blob/master/jquery.visible.js
  1238. # to have not dependency, port to coffeescript
  1239. isVisible: (el, partial, hidden, direction) ->
  1240. return if el.length < 1
  1241. vpWidth = window.innerWidth
  1242. vpHeight = window.innerHeight
  1243. direction = if direction then direction else 'both'
  1244. clientSize = if hidden is true then t.offsetWidth * t.offsetHeight else true
  1245. rec = el.getBoundingClientRect()
  1246. tViz = rec.top >= 0 && rec.top < vpHeight
  1247. bViz = rec.bottom > 0 && rec.bottom <= vpHeight
  1248. lViz = rec.left >= 0 && rec.left < vpWidth
  1249. rViz = rec.right > 0 && rec.right <= vpWidth
  1250. vVisible = if partial then tViz || bViz else tViz && bViz
  1251. hVisible = if partial then lViz || rViz else lViz && rViz
  1252. if direction is 'both'
  1253. return clientSize && vVisible && hVisible
  1254. else if direction is 'vertical'
  1255. return clientSize && vVisible
  1256. else if direction is 'horizontal'
  1257. return clientSize && hVisible
  1258. isRetina: ->
  1259. if window.matchMedia
  1260. 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)')
  1261. return (mq && mq.matches || (window.devicePixelRatio > 1))
  1262. false
  1263. resizeImage: (dataURL, x = 'auto', y = 'auto', sizeFactor = 1, type, quallity, callback, force = true) ->
  1264. # load image from data url
  1265. imageObject = new Image()
  1266. imageObject.onload = ->
  1267. imageWidth = imageObject.width
  1268. imageHeight = imageObject.height
  1269. console.log('ImageService', 'current size', imageWidth, imageHeight)
  1270. if y is 'auto' && x is 'auto'
  1271. x = imageWidth
  1272. y = imageHeight
  1273. # get auto dimensions
  1274. if y is 'auto'
  1275. factor = imageWidth / x
  1276. y = imageHeight / factor
  1277. if x is 'auto'
  1278. factor = imageWidth / y
  1279. x = imageHeight / factor
  1280. # check if resize is needed
  1281. resize = false
  1282. if x < imageWidth || y < imageHeight
  1283. resize = true
  1284. x = x * sizeFactor
  1285. y = y * sizeFactor
  1286. else
  1287. x = imageWidth
  1288. y = imageHeight
  1289. # create canvas and set dimensions
  1290. canvas = document.createElement('canvas')
  1291. canvas.width = x
  1292. canvas.height = y
  1293. # draw image on canvas and set image dimensions
  1294. context = canvas.getContext('2d')
  1295. context.drawImage(imageObject, 0, 0, x, y)
  1296. # set quallity based on image size
  1297. if quallity == 'auto'
  1298. if x < 200 && y < 200
  1299. quallity = 1
  1300. else if x < 400 && y < 400
  1301. quallity = 0.9
  1302. else if x < 600 && y < 600
  1303. quallity = 0.8
  1304. else if x < 900 && y < 900
  1305. quallity = 0.7
  1306. else
  1307. quallity = 0.6
  1308. # execute callback with resized image
  1309. newDataUrl = canvas.toDataURL(type, quallity)
  1310. if resize
  1311. console.log('ImageService', 'resize', x/sizeFactor, y/sizeFactor, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
  1312. callback(newDataUrl, x/sizeFactor, y/sizeFactor, true)
  1313. return
  1314. console.log('ImageService', 'no resize', x, y, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
  1315. callback(newDataUrl, x, y, false)
  1316. # load image from data url
  1317. imageObject.src = dataURL
  1318. # taken from https://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
  1319. pasteHtmlAtCaret: (html) ->
  1320. sel = undefined
  1321. range = undefined
  1322. if window.getSelection
  1323. sel = window.getSelection()
  1324. if sel.getRangeAt && sel.rangeCount
  1325. range = sel.getRangeAt(0)
  1326. range.deleteContents()
  1327. el = document.createElement('div')
  1328. el.innerHTML = html
  1329. frag = document.createDocumentFragment(node, lastNode)
  1330. while node = el.firstChild
  1331. lastNode = frag.appendChild(node)
  1332. range.insertNode(frag)
  1333. if lastNode
  1334. range = range.cloneRange()
  1335. range.setStartAfter(lastNode)
  1336. range.collapse(true)
  1337. sel.removeAllRanges()
  1338. sel.addRange(range)
  1339. else if document.selection && document.selection.type != 'Control'
  1340. document.selection.createRange().pasteHTML(html)
  1341. # (C) sbrin - https://github.com/sbrin
  1342. # https://gist.github.com/sbrin/6801034
  1343. wordFilter: (editor) ->
  1344. content = editor.html()
  1345. # Word comments like conditional comments etc
  1346. content = content.replace(/<!--[\s\S]+?-->/gi, '')
  1347. # Remove comments, scripts (e.g., msoShowComment), XML tag, VML content,
  1348. # MS Office namespaced tags, and a few other tags
  1349. content = content.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, '')
  1350. # Convert <s> into <strike> for line-though
  1351. content = content.replace(/<(\/?)s>/gi, '<$1strike>')
  1352. # Replace nbsp entites to char since it's easier to handle
  1353. # content = content.replace(/&nbsp;/gi, "\u00a0")
  1354. content = content.replace(/&nbsp;/gi, ' ')
  1355. # Convert <span style="mso-spacerun:yes">___</span> to string of alternating
  1356. # breaking/non-breaking spaces of same length
  1357. #content = content.replace(/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi, (str, spaces) ->
  1358. # return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : ''
  1359. #)
  1360. editor.innerHTML = content
  1361. # Parse out list indent level for lists
  1362. for p in editor.querySelectorAll('p')
  1363. str = p.getAttribute('style')
  1364. matches = /mso-list:\w+ \w+([0-9]+)/.exec(str)
  1365. if matches
  1366. p.dataset._listLevel = parseInt(matches[1], 10)
  1367. # Parse Lists
  1368. last_level = 0
  1369. pnt = null
  1370. for p in editor.querySelectorAll('p')
  1371. cur_level = p.dataset._listLevel
  1372. if cur_level != undefined
  1373. txt = p.textContent
  1374. list_tag = '<ul></ul>'
  1375. if (/^\s*\w+\./.test(txt))
  1376. matches = /([0-9])\./.exec(txt)
  1377. if matches
  1378. start = parseInt(matches[1], 10)
  1379. list_tag = start>1 ? '<ol start="' + start + '"></ol>' : '<ol></ol>'
  1380. else
  1381. list_tag = '<ol></ol>'
  1382. if cur_level > last_level
  1383. if last_level == 0
  1384. p.insertAdjacentHTML 'beforebegin', list_tag
  1385. pnt = p.previousElementSibling
  1386. else
  1387. pnt.insertAdjacentHTML 'beforeend', list_tag
  1388. if cur_level < last_level
  1389. for i in [i..last_level-cur_level]
  1390. pnt = pnt.parentNode
  1391. p.querySelector('span:first').remove() if p.querySelector('span:first')
  1392. pnt.insertAdjacentHTML 'beforeend', '<li>' + p.innerHTML + '</li>'
  1393. p.remove()
  1394. last_level = cur_level
  1395. else
  1396. last_level = 0
  1397. el.removeAttribute('style') for el in editor.querySelectorAll('[style]')
  1398. el.removeAttribute('align') for el in editor.querySelectorAll('[align]')
  1399. el.outerHTML = el.innerHTML for el in editor.querySelectorAll('span')
  1400. el.remove() for el in editor.querySelectorAll('span:empty')
  1401. el.removeAttribute('class') for el in editor.querySelectorAll("[class^='Mso']")
  1402. el.remove() for el in editor.querySelectorAll('p:empty')
  1403. editor
  1404. removeAttribute: (element) ->
  1405. return if !element
  1406. for att in element.attributes
  1407. element.removeAttribute(att.name)
  1408. removeAttributes: (html) =>
  1409. for node in html.querySelectorAll('*')
  1410. @removeAttribute node
  1411. html
  1412. window.ZammadChat = ZammadChat