chat-no-jquery.coffee 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641
  1. do(window) ->
  2. scripts = document.getElementsByTagName('script')
  3. # search for script to get protocol and hostname for ws connection
  4. myScript = scripts[scripts.length - 1]
  5. scriptProtocol = window.location.protocol.replace(':', '') # set default protocol
  6. if myScript && myScript.src
  7. scriptHost = myScript.src.match('.*://([^:/]*).*')[1]
  8. scriptProtocol = myScript.src.match('(.*)://[^:/]*.*')[1]
  9. # Define the plugin class
  10. class Core
  11. defaults:
  12. debug: false
  13. constructor: (options) ->
  14. @options = {}
  15. for key, value of @defaults
  16. @options[key] = value
  17. for key, value of options
  18. @options[key] = value
  19. class Base extends Core
  20. constructor: (options) ->
  21. super(options)
  22. @log = new Log(debug: @options.debug, logPrefix: @options.logPrefix || @logPrefix)
  23. class Log extends Core
  24. debug: (items...) =>
  25. return if !@options.debug
  26. @log('debug', items)
  27. notice: (items...) =>
  28. @log('notice', items)
  29. error: (items...) =>
  30. @log('error', items)
  31. log: (level, items) =>
  32. items.unshift('||')
  33. items.unshift(level)
  34. items.unshift(@options.logPrefix)
  35. console.log.apply console, items
  36. return if !@options.debug
  37. logString = ''
  38. for item in items
  39. logString += ' '
  40. if typeof item is 'object'
  41. logString += JSON.stringify(item)
  42. else if item && item.toString
  43. logString += item.toString()
  44. else
  45. logString += item
  46. element = document.querySelector('.js-chatLogDisplay')
  47. if element
  48. element.innerHTML = '<div>' + logString + '</div>' + element.innerHTML
  49. class Timeout extends Base
  50. timeoutStartedAt: null
  51. logPrefix: 'timeout'
  52. defaults:
  53. debug: false
  54. timeout: 4
  55. timeoutIntervallCheck: 0.5
  56. start: =>
  57. @stop()
  58. timeoutStartedAt = new Date
  59. check = =>
  60. timeLeft = new Date - new Date(timeoutStartedAt.getTime() + @options.timeout * 1000 * 60)
  61. @log.debug "Timeout check for #{@options.timeout} minutes (left #{timeLeft/1000} sec.)"#, new Date
  62. return if timeLeft < 0
  63. @stop()
  64. @options.callback()
  65. @log.debug "Start timeout in #{@options.timeout} minutes"#, new Date
  66. @intervallId = setInterval(check, @options.timeoutIntervallCheck * 1000 * 60)
  67. stop: =>
  68. return if !@intervallId
  69. @log.debug "Stop timeout of #{@options.timeout} minutes"#, new Date
  70. clearInterval(@intervallId)
  71. class Io extends Base
  72. logPrefix: 'io'
  73. set: (params) =>
  74. for key, value of params
  75. @options[key] = value
  76. connect: =>
  77. @log.debug "Connecting to #{@options.host}"
  78. @ws = new window.WebSocket("#{@options.host}")
  79. @ws.onopen = (e) =>
  80. @log.debug 'onOpen', e
  81. @options.onOpen(e)
  82. @ping()
  83. @ws.onmessage = (e) =>
  84. pipes = JSON.parse(e.data)
  85. @log.debug 'onMessage', e.data
  86. for pipe in pipes
  87. if pipe.event is 'pong'
  88. @ping()
  89. if @options.onMessage
  90. @options.onMessage(pipes)
  91. @ws.onclose = (e) =>
  92. @log.debug 'close websocket connection', e
  93. if @pingDelayId
  94. clearTimeout(@pingDelayId)
  95. if @manualClose
  96. @log.debug 'manual close, onClose callback'
  97. @manualClose = false
  98. if @options.onClose
  99. @options.onClose(e)
  100. else
  101. @log.debug 'error close, onError callback'
  102. if @options.onError
  103. @options.onError('Connection lost...')
  104. @ws.onerror = (e) =>
  105. @log.debug 'onError', e
  106. if @options.onError
  107. @options.onError(e)
  108. close: =>
  109. @log.debug 'close websocket manually'
  110. @manualClose = true
  111. @ws.close()
  112. reconnect: =>
  113. @log.debug 'reconnect'
  114. @close()
  115. @connect()
  116. send: (event, data = {}) =>
  117. @log.debug 'send', event, data
  118. msg = JSON.stringify
  119. event: event
  120. data: data
  121. @ws.send msg
  122. ping: =>
  123. localPing = =>
  124. @send('ping')
  125. @pingDelayId = setTimeout(localPing, 29000)
  126. class ZammadChat extends Base
  127. defaults:
  128. chatId: undefined
  129. show: true
  130. target: document.querySelector('body')
  131. host: ''
  132. debug: false
  133. flat: false
  134. lang: undefined
  135. cssAutoload: true
  136. cssUrl: undefined
  137. fontSize: undefined
  138. buttonClass: 'open-zammad-chat'
  139. inactiveClass: 'is-inactive'
  140. title: '<strong>Chat</strong> with us!'
  141. scrollHint: 'Scroll down to see new messages'
  142. idleTimeout: 6
  143. idleTimeoutIntervallCheck: 0.5
  144. inactiveTimeout: 8
  145. inactiveTimeoutIntervallCheck: 0.5
  146. waitingListTimeout: 4
  147. waitingListTimeoutIntervallCheck: 0.5
  148. # Callbacks
  149. onReady: undefined
  150. onCloseAnimationEnd: undefined
  151. onError: undefined
  152. onOpenAnimationEnd: undefined
  153. onConnectionReestablished: undefined
  154. onSessionClosed: undefined
  155. onConnectionEstablished: undefined
  156. onCssLoaded: undefined
  157. logPrefix: 'chat'
  158. _messageCount: 0
  159. isOpen: false
  160. blinkOnlineInterval: null
  161. stopBlinOnlineStateTimeout: null
  162. showTimeEveryXMinutes: 2
  163. lastTimestamp: null
  164. lastAddedType: null
  165. inputDisabled: false
  166. inputTimeout: null
  167. isTyping: false
  168. state: 'offline'
  169. initialQueueDelay: 10000
  170. translations:
  171. # ZAMMAD_TRANSLATIONS_START
  172. 'cs':
  173. '<strong>Chat</strong> with us!': '<strong>Chatujte</strong> s námi!'
  174. 'All colleagues are busy.': 'Všichni kolegové jsou vytíženi.'
  175. 'Chat closed by %s': '%s ukončil konverzaci'
  176. 'Compose your message…': 'Napište svou zprávu…'
  177. 'Connecting': 'Připojování'
  178. 'Connection lost': 'Připojení ztraceno'
  179. 'Connection re-established': 'Připojení obnoveno'
  180. 'Offline': 'Offline'
  181. 'Online': 'Online'
  182. 'Scroll down to see new messages': 'Srolujte dolů pro zobrazení nových zpráv'
  183. 'Send': 'Odeslat'
  184. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Jelikož jste nereagovali v posledních %s minutách, vaše konverzace byla uzavřena.'
  185. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Jelikož jste nereagovali v posledních %s minutách, vaše konverzace s <strong>%s</strong> byla uzavřena.'
  186. 'Start new conversation': 'Zahájit novou konverzaci'
  187. 'Today': 'Dnes'
  188. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Omlouváme se, že musíte čekat déle, než je vhodné pro získání slotu. Prosím, zkuste to později, případně nám napište e-mail. Děkujeme!'
  189. 'You are on waiting list position <strong>%s</strong>.': 'Jste <strong>%s</strong>. v pořadí na čekací listině.'
  190. 'da':
  191. '<strong>Chat</strong> with us!': '<strong>Chat</strong> med os!'
  192. 'All colleagues are busy.': 'Alle medarbejdere er optaget.'
  193. 'Chat closed by %s': 'Chat lukket af %s'
  194. 'Compose your message…': 'Skriv din besked…'
  195. 'Connecting': 'Forbinder'
  196. 'Connection lost': 'Forbindelse mistet'
  197. 'Connection re-established': 'Forbindelse genoprettet'
  198. 'Offline': 'Offline'
  199. 'Online': 'Online'
  200. 'Scroll down to see new messages': 'Scroll ned for at se nye beskeder'
  201. 'Send': 'Afsend'
  202. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  203. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  204. 'Start new conversation': 'Start en ny samtale'
  205. 'Today': 'I dag'
  206. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': ''
  207. 'You are on waiting list position <strong>%s</strong>.': 'Du er i kø som nummer <strong>%s</strong>.'
  208. 'de':
  209. '<strong>Chat</strong> with us!': '<strong>Chatte</strong> mit uns!'
  210. 'All colleagues are busy.': 'Alle Kollegen sind beschäftigt.'
  211. 'Chat closed by %s': 'Chat von %s geschlossen'
  212. 'Compose your message…': 'Verfassen Sie Ihre Nachricht…'
  213. 'Connecting': 'Verbinde'
  214. 'Connection lost': 'Verbindung verloren'
  215. 'Connection re-established': 'Verbindung wieder aufgebaut'
  216. 'Offline': 'Offline'
  217. 'Online': 'Online'
  218. 'Scroll down to see new messages': 'Nach unten scrollen um neue Nachrichten zu sehen'
  219. 'Send': 'Senden'
  220. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Da Sie innerhalb der letzten %s Minuten nicht reagiert haben, wurde Ihre Unterhaltung geschlossen.'
  221. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Da Sie innerhalb der letzten %s Minuten nicht reagiert haben, wurde Ihre Unterhaltung mit <strong>%s</strong> geschlossen.'
  222. 'Start new conversation': 'Neue Unterhaltung starten'
  223. 'Today': 'Heute'
  224. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Entschuldigung, es dauert länger als erwartet einen freien Platz zu bekommen. Versuchen Sie es später erneut oder senden Sie uns eine E-Mail. Vielen Dank!'
  225. 'You are on waiting list position <strong>%s</strong>.': 'Sie sind in der Warteliste auf Position <strong>%s</strong>.'
  226. 'es':
  227. '<strong>Chat</strong> with us!': '<strong>Chatee</strong> con nosotros!'
  228. 'All colleagues are busy.': 'Todos los colegas están ocupados.'
  229. 'Chat closed by %s': 'Chat cerrado por %s'
  230. 'Compose your message…': 'Escribe tu mensaje…'
  231. 'Connecting': 'Conectando'
  232. 'Connection lost': 'Conexión perdida'
  233. 'Connection re-established': 'Conexión reestablecida'
  234. 'Offline': 'Desconectado'
  235. 'Online': 'En línea'
  236. 'Scroll down to see new messages': 'Desplace hacia abajo para ver nuevos mensajes'
  237. 'Send': 'Enviar'
  238. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Debido a que usted no ha respondido en los últimos %s minutos, su conversación se ha cerrado.'
  239. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Debido a que usted no ha respondido en los últimos %s minutos, su conversación con <strong>%s</strong> se ha cerrado.'
  240. 'Start new conversation': 'Iniciar nueva conversación'
  241. 'Today': 'Hoy'
  242. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Lo sentimos, estamos tardando más de lo esperado para asignar un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!'
  243. 'You are on waiting list position <strong>%s</strong>.': 'Usted está en la posición <strong>%s</strong> de la lista de espera.'
  244. 'fr':
  245. '<strong>Chat</strong> with us!': '<strong>Chattez</strong> avec nous !'
  246. 'All colleagues are busy.': 'Tous les agents sont occupés.'
  247. 'Chat closed by %s': 'Chat fermé par %s'
  248. 'Compose your message…': 'Ecrivez votre message…'
  249. 'Connecting': 'Connexion'
  250. 'Connection lost': 'Connexion perdue'
  251. 'Connection re-established': 'Connexion ré-établie'
  252. 'Offline': 'Hors-ligne'
  253. 'Online': 'En ligne'
  254. 'Scroll down to see new messages': 'Défiler vers le bas pour voir les nouveaux messages'
  255. 'Send': 'Envoyer'
  256. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  257. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  258. 'Start new conversation': 'Démarrer une nouvelle conversation'
  259. 'Today': 'Aujourd\'hui'
  260. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': ''
  261. 'You are on waiting list position <strong>%s</strong>.': 'Vous êtes actuellement en position <strong>%s</strong> dans la file d\'attente.'
  262. 'hr':
  263. '<strong>Chat</strong> with us!': '<strong>Čavrljajte</strong> sa nama!'
  264. 'All colleagues are busy.': 'Svi kolege su zauzeti.'
  265. 'Chat closed by %s': '%s zatvara chat'
  266. 'Compose your message…': 'Sastavite poruku…'
  267. 'Connecting': 'Povezivanje'
  268. 'Connection lost': 'Veza prekinuta'
  269. 'Connection re-established': 'Veza je ponovno uspostavljena'
  270. 'Offline': 'Odsutan'
  271. 'Online': 'Dostupan(a)'
  272. 'Scroll down to see new messages': 'Pomaknite se prema dolje da biste vidjeli nove poruke'
  273. 'Send': 'Šalji'
  274. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Budući da niste odgovorili u posljednjih %s minuta, Vaš je razgovor zatvoren.'
  275. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Budući da niste odgovorili u posljednjih %s minuta, Vaš je razgovor s <strong>%</strong>s zatvoren.'
  276. 'Start new conversation': 'Započni novi razgovor'
  277. 'Today': 'Danas'
  278. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Oprostite, proces traje duže nego što se očekivalo da biste dobili slobodan termin. Molimo, pokušajte ponovno kasnije ili nam pošaljite e-mail. Hvala!'
  279. 'You are on waiting list position <strong>%s</strong>.': 'Nalazite se u redu čekanja na poziciji <strong>%s</strong>.'
  280. 'hu':
  281. '<strong>Chat</strong> with us!': '<strong>Csevegjen</strong> velünk!'
  282. 'All colleagues are busy.': 'Minden munkatársunk foglalt.'
  283. 'Chat closed by %s': 'A csevegés %s által lezárva'
  284. 'Compose your message…': 'Fogalmazza meg üzenetét…'
  285. 'Connecting': 'Csatlakozás'
  286. 'Connection lost': 'A kapcsolat megszakadt'
  287. 'Connection re-established': 'A kapcsolat helyreállt'
  288. 'Offline': 'Offline'
  289. 'Online': 'Online'
  290. 'Scroll down to see new messages': 'Görgessen lefelé az új üzenetek megtekintéséhez'
  291. 'Send': 'Küldés'
  292. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Mivel az elmúlt %s percben nem válaszolt, a beszélgetése lezárásra került.'
  293. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Mivel az elmúlt %s percben nem válaszolt, <strong>%s</strong> munkatársunkkal folytatott beszélgetését lezártuk.'
  294. 'Start new conversation': 'Új beszélgetés indítása'
  295. 'Today': 'Ma'
  296. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Sajnáljuk, hogy a vártnál hosszabb ideig tart a helyfoglalás. Kérjük, próbálja meg később újra, vagy küldjön nekünk egy e-mailt. Köszönjük!'
  297. 'You are on waiting list position <strong>%s</strong>.': 'Ön a várólistán a <strong>%s</strong> helyen szerepel.'
  298. 'it':
  299. '<strong>Chat</strong> with us!': '<strong>Chatta</strong> con noi!'
  300. 'All colleagues are busy.': 'Tutti i colleghi sono occupati.'
  301. 'Chat closed by %s': 'Chat chiusa da %s'
  302. 'Compose your message…': 'Scrivi il tuo messaggio…'
  303. 'Connecting': 'Connessione in corso'
  304. 'Connection lost': 'Connessione persa'
  305. 'Connection re-established': 'Connessione ristabilita'
  306. 'Offline': 'Offline'
  307. 'Online': 'Online'
  308. 'Scroll down to see new messages': 'Scorri verso il basso per vedere i nuovi messaggi'
  309. 'Send': 'Invia'
  310. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Dato che non hai risposto negli ultimi %s minuti, la conversazione è stata chiusa.'
  311. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Dato che non hai risposto negli ultimi %s minuti, la conversazione con <strong>%s</strong> è stata chiusa.'
  312. 'Start new conversation': 'Avvia una nuova chat'
  313. 'Today': 'Oggi'
  314. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Siamo spiacenti, ci vuole più tempo del previsto per ottenere uno spazio libero. Riprova più tardi o inviaci un\'e-mail. Grazie!'
  315. 'You are on waiting list position <strong>%s</strong>.': 'Sei alla posizione <strong>%s</strong> della lista di attesa.'
  316. 'nl':
  317. '<strong>Chat</strong> with us!': '<strong>Chat</strong> met ons!'
  318. 'All colleagues are busy.': 'Alle collega\'s zijn bezet.'
  319. 'Chat closed by %s': 'Chat gesloten door %s'
  320. 'Compose your message…': 'Stel je bericht op…'
  321. 'Connecting': 'Verbinden'
  322. 'Connection lost': 'Verbinding verbroken'
  323. 'Connection re-established': 'Verbinding hersteld'
  324. 'Offline': 'Offline'
  325. 'Online': 'Online'
  326. 'Scroll down to see new messages': 'Scroll naar beneden om nieuwe tickets te bekijken'
  327. 'Send': 'Verstuur'
  328. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'De chat is afgesloten omdat je de laatste %s minuten niet hebt gereageerd.'
  329. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Je chat met <strong>%s</strong> is afgesloten omdat je niet hebt gereageerd in de laatste %s minuten.'
  330. 'Start new conversation': 'Nieuw gesprek starten'
  331. 'Today': 'Vandaag'
  332. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Het spijt ons, het duurt langer dan verwacht om een chat te starten. Probeer het later nog eens of stuur ons een e-mail. Bedankt!'
  333. 'You are on waiting list position <strong>%s</strong>.': 'Je bevindt zich op wachtlijstpositie <strong>%s</strong>.'
  334. 'pl':
  335. '<strong>Chat</strong> with us!': '<strong>Czatuj</strong> z nami!'
  336. 'All colleagues are busy.': 'Wszyscy agenci są zajęci.'
  337. 'Chat closed by %s': 'Chat zamknięty przez %s'
  338. 'Compose your message…': 'Skomponuj swoją wiadomość…'
  339. 'Connecting': 'Łączenie'
  340. 'Connection lost': 'Utracono połączenie'
  341. 'Connection re-established': 'Ponowne nawiązanie połączenia'
  342. 'Offline': 'Offline'
  343. 'Online': 'Online'
  344. 'Scroll down to see new messages': 'Skroluj w dół, aby zobaczyć wiadomości'
  345. 'Send': 'Wyślij'
  346. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.'
  347. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa z <strong>%s</strong> została zamknięta.'
  348. 'Start new conversation': 'Rozpocznij nową rozmowę'
  349. 'Today': 'Dzisiaj'
  350. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Przepraszamy, znalezienie wolnego konsultanta zajmuje więcej czasu niż oczekiwano. Spróbuj ponownie później lub wyślij nam e-mail. Dziękujemy!'
  351. 'You are on waiting list position <strong>%s</strong>.': 'Jesteś na pozycji listy oczekujących <strong>%s</strong>.'
  352. 'pt-br':
  353. '<strong>Chat</strong> with us!': '<strong>Converse</strong> conosco!'
  354. 'All colleagues are busy.': 'Nossos atendentes estão ocupados.'
  355. 'Chat closed by %s': 'Chat encerrado por %s'
  356. 'Compose your message…': 'Escreva sua mensagem…'
  357. 'Connecting': 'Conectando'
  358. 'Connection lost': 'Conexão perdida'
  359. 'Connection re-established': 'Conexão restabelecida'
  360. 'Offline': 'Desconectado'
  361. 'Online': 'Online'
  362. 'Scroll down to see new messages': 'Rolar para baixo para ver novas mensagems'
  363. 'Send': 'Enviar'
  364. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Como você não respondeu nos últimos %s minutos, sua conversa foi encerrada.'
  365. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Como você não respondeu nos últimos %s minutos, sua conversa com <strong>%s</strong> foi encerrada.'
  366. 'Start new conversation': 'Iniciar uma nova conversa'
  367. 'Today': 'Hoje'
  368. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Lamentamos, está demorando mais do que o esperado para conseguir uma vaga. Tente novamente mais tarde ou envie-nos um e-mail. Obrigado!'
  369. 'You are on waiting list position <strong>%s</strong>.': 'Você está na posição <strong>%s</strong> da lista de espera.'
  370. 'ru':
  371. '<strong>Chat</strong> with us!': '<strong>Напишите</strong> нам!'
  372. 'All colleagues are busy.': 'Все коллеги заняты.'
  373. 'Chat closed by %s': 'Чат закрыт %s'
  374. 'Compose your message…': 'Составьте сообщение…'
  375. 'Connecting': 'Подключение'
  376. 'Connection lost': 'Подключение потеряно'
  377. 'Connection re-established': 'Подключение восстановлено'
  378. 'Offline': 'Оффлайн'
  379. 'Online': 'В сети'
  380. 'Scroll down to see new messages': 'Прокрутите вниз, чтобы увидеть новые сообщения'
  381. 'Send': 'Отправить'
  382. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  383. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  384. 'Start new conversation': 'Начать новую беседу'
  385. 'Today': 'Сегодня'
  386. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': ''
  387. 'You are on waiting list position <strong>%s</strong>.': 'Вы находитесь в списке ожидания <strong>%s</strong>.'
  388. 'sr':
  389. '<strong>Chat</strong> with us!': '<strong>Ћаскајте</strong> са нама!'
  390. 'All colleagues are busy.': 'Све колеге су заузете.'
  391. 'Chat closed by %s': 'Ћаскање затворено од стране %s'
  392. 'Compose your message…': 'Напишите поруку…'
  393. 'Connecting': 'Повезивање'
  394. 'Connection lost': 'Веза је изгубљена'
  395. 'Connection re-established': 'Веза је поново успостављена'
  396. 'Offline': 'Одсутан(а)'
  397. 'Online': 'Доступан(а)'
  398. 'Scroll down to see new messages': 'Скролујте на доле за нове поруке'
  399. 'Send': 'Пошаљи'
  400. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Пошто нисте одговорили у последњих %s минут(a), ваш разговор је завршен.'
  401. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Пошто нисте одговорили у последњих %s минут(a), ваш разговор са <strong>%s</strong> је завршен.'
  402. 'Start new conversation': 'Започни нови разговор'
  403. 'Today': 'Данас'
  404. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Жао нам је, добијање празног термина траје дуже од очекиваног. Молимо покушајте поново касније или нам пошаљите имејл поруку. Хвала вам!'
  405. 'You are on waiting list position <strong>%s</strong>.': 'Ви сте тренутно <strong>%s.</strong> у реду за чекање.'
  406. 'sr-latn-rs':
  407. '<strong>Chat</strong> with us!': '<strong>Ćaskajte</strong> sa nama!'
  408. 'All colleagues are busy.': 'Sve kolege su zauzete.'
  409. 'Chat closed by %s': 'Ćaskanje zatvoreno od strane %s'
  410. 'Compose your message…': 'Napišite poruku…'
  411. 'Connecting': 'Povezivanje'
  412. 'Connection lost': 'Veza je izgubljena'
  413. 'Connection re-established': 'Veza je ponovo uspostavljena'
  414. 'Offline': 'Odsutan(a)'
  415. 'Online': 'Dostupan(a)'
  416. 'Scroll down to see new messages': 'Skrolujte na dole za nove poruke'
  417. 'Send': 'Pošalji'
  418. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': 'Pošto niste odgovorili u poslednjih %s minut(a), vaš razgovor je završen.'
  419. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': 'Pošto niste odgovorili u poslednjih %s minut(a), vaš razgovor sa <strong>%s</strong> je završen.'
  420. 'Start new conversation': 'Započni novi razgovor'
  421. 'Today': 'Danas'
  422. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Žao nam je, dobijanje praznog termina traje duže od očekivanog. Molimo pokušajte ponovo kasnije ili nam pošaljite imejl poruku. Hvala vam!'
  423. 'You are on waiting list position <strong>%s</strong>.': 'Vi ste trenutno <strong>%s.</strong> u redu za čekanje.'
  424. 'sv':
  425. '<strong>Chat</strong> with us!': '<strong>Chatta</strong> med oss!'
  426. 'All colleagues are busy.': 'Alla kollegor är upptagna.'
  427. 'Chat closed by %s': 'Chatt stängd av %s'
  428. 'Compose your message…': 'Skriv ditt meddelande …'
  429. 'Connecting': 'Ansluter'
  430. 'Connection lost': 'Anslutningen försvann'
  431. 'Connection re-established': 'Anslutningen återupprättas'
  432. 'Offline': 'Offline'
  433. 'Online': 'Online'
  434. 'Scroll down to see new messages': 'Bläddra ner för att se nya meddelanden'
  435. 'Send': 'Skicka'
  436. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  437. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  438. 'Start new conversation': 'Starta ny konversation'
  439. 'Today': 'Idag'
  440. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': 'Det tar tyvärr längre tid än förväntat att få en ledig plats. Försök igen senare eller skicka ett mejl till oss. Tack!'
  441. 'You are on waiting list position <strong>%s</strong>.': 'Du är på väntelistan som position <strong>%s</strong>.'
  442. 'zh-cn':
  443. '<strong>Chat</strong> with us!': '发起<strong>即时对话</strong>!'
  444. 'All colleagues are busy.': '所有同事都很忙。'
  445. 'Chat closed by %s': '对话被 %s 终止'
  446. 'Compose your message…': '编辑您的信息…'
  447. 'Connecting': '连接中'
  448. 'Connection lost': '连接丢失'
  449. 'Connection re-established': '正在重新建立连接'
  450. 'Offline': '离线'
  451. 'Online': '在线'
  452. 'Scroll down to see new messages': '向下滚动以查看新消息'
  453. 'Send': '发送'
  454. 'Since you didn\'t respond in the last %s minutes your conversation was closed.': ''
  455. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> was closed.': ''
  456. 'Start new conversation': '开始新的会话'
  457. 'Today': '今天'
  458. 'We are sorry, it is taking longer than expected to get a slot. Please try again later or send us an email. Thank you!': ''
  459. 'You are on waiting list position <strong>%s</strong>.': '您目前的等候位置是第 <strong>%s</strong> 位.'
  460. # ZAMMAD_TRANSLATIONS_END
  461. sessionId: undefined
  462. scrolledToBottom: true
  463. scrollSnapTolerance: 10
  464. richTextFormatKey:
  465. 66: true # b
  466. 73: true # i
  467. 85: true # u
  468. 83: true # s
  469. T: (string, items...) =>
  470. if @options.lang && @options.lang isnt 'en'
  471. if !@translations[@options.lang]
  472. @log.notice "Translation '#{@options.lang}' needed!"
  473. else
  474. translations = @translations[@options.lang]
  475. if !translations[string]
  476. @log.notice "Translation needed for '#{string}'"
  477. string = translations[string] || string
  478. if items
  479. for item in items
  480. string = string.replace(/%s/, item)
  481. string
  482. view: (name) =>
  483. return (options) =>
  484. if !options
  485. options = {}
  486. options.T = @T
  487. options.background = @options.background
  488. options.flat = @options.flat
  489. options.fontSize = @options.fontSize
  490. return window.zammadChatTemplates[name](options)
  491. constructor: (options) ->
  492. super(options)
  493. # jQuery migration
  494. if typeof jQuery != 'undefined' && @options.target instanceof jQuery
  495. @log.notice 'Chat: target option is a jQuery object. jQuery is not a requirement for the chat any more.'
  496. @options.target = @options.target.get(0)
  497. # fullscreen
  498. @isFullscreen = (window.matchMedia and window.matchMedia('(max-width: 768px)').matches)
  499. @scrollRoot = @getScrollRoot()
  500. # check prerequisites
  501. if !window.WebSocket or !sessionStorage
  502. @state = 'unsupported'
  503. @log.notice 'Chat: Browser not supported!'
  504. return
  505. if !@options.chatId
  506. @state = 'unsupported'
  507. @log.error 'Chat: need chatId as option!'
  508. return
  509. # detect language
  510. if !@options.lang
  511. @options.lang = document.documentElement.getAttribute('lang')
  512. if @options.lang
  513. if !@translations[@options.lang]
  514. @log.debug "lang: No #{@options.lang} found, try first two letters"
  515. @options.lang = @options.lang.replace(/-.+?$/, '') # replace "-xx" of xx-xx
  516. @log.debug "lang: #{@options.lang}"
  517. # detect host
  518. @detectHost() if !@options.host
  519. @loadCss()
  520. @io = new Io(@options)
  521. @io.set(
  522. onOpen: @render
  523. onClose: @onWebSocketClose
  524. onMessage: @onWebSocketMessage
  525. onError: @onError
  526. )
  527. @io.connect()
  528. getScrollRoot: ->
  529. return document.scrollingElement if 'scrollingElement' of document
  530. html = document.documentElement
  531. start = parseInt(html.pageYOffset, 10)
  532. html.pageYOffset = start + 1
  533. end = parseInt(html.pageYOffset, 10)
  534. html.pageYOffset = start
  535. return if end > start then html else document.body
  536. render: =>
  537. if !@el || !document.querySelector('.zammad-chat')
  538. @renderBase()
  539. # disable open button
  540. btn = document.querySelector(".#{ @options.buttonClass }")
  541. if btn
  542. btn.classList.add @options.inactiveClass
  543. @setAgentOnlineState 'online'
  544. @log.debug 'widget rendered'
  545. @startTimeoutObservers()
  546. @idleTimeout.start()
  547. # get current chat status
  548. @sessionId = sessionStorage.getItem('sessionId')
  549. @send 'chat_status_customer',
  550. session_id: @sessionId
  551. url: window.location.href
  552. renderBase: ->
  553. @el.remove() if @el
  554. @options.target.insertAdjacentHTML('beforeend', @view('chat')(
  555. title: @options.title,
  556. scrollHint: @options.scrollHint
  557. ))
  558. @el = @options.target.querySelector('.zammad-chat')
  559. @input = @el.querySelector('.zammad-chat-input')
  560. @body = @el.querySelector('.zammad-chat-body')
  561. # start bindings
  562. @el.querySelector('.js-chat-open').addEventListener('click', @open)
  563. @el.querySelector('.js-chat-toggle').addEventListener('click', @toggle)
  564. @el.querySelector('.js-chat-status').addEventListener('click', @stopPropagation)
  565. @el.querySelector('.zammad-chat-controls').addEventListener('submit', @onSubmit)
  566. @body.addEventListener('scroll', @detectScrolledtoBottom)
  567. @el.querySelector('.zammad-scroll-hint').addEventListener('click', @onScrollHintClick)
  568. @input.addEventListener('keydown', @onKeydown)
  569. @input.addEventListener('input', @onInput)
  570. @input.addEventListener('paste', @onPaste)
  571. @input.addEventListener('drop', @onDrop)
  572. window.addEventListener('beforeunload', @onLeaveTemporary)
  573. window.addEventListener('hashchange', =>
  574. if @isOpen
  575. if @sessionId
  576. @send 'chat_session_notice',
  577. session_id: @sessionId
  578. message: window.location.href
  579. return
  580. @idleTimeout.start()
  581. )
  582. stopPropagation: (event) ->
  583. event.stopPropagation()
  584. onDrop: (e) =>
  585. e.stopPropagation()
  586. e.preventDefault()
  587. if window.dataTransfer # ie
  588. dataTransfer = window.dataTransfer
  589. else if e.dataTransfer # other browsers
  590. dataTransfer = e.dataTransfer
  591. else
  592. throw 'No clipboardData support'
  593. x = e.clientX
  594. y = e.clientY
  595. file = dataTransfer.files[0]
  596. # look for images
  597. if file.type.match('image.*')
  598. reader = new FileReader()
  599. reader.onload = (e) =>
  600. # Insert the image at the carat
  601. insert = (dataUrl, width) =>
  602. # adapt image if we are on retina devices
  603. if @isRetina()
  604. width = width / 2
  605. result = dataUrl
  606. img = new Image()
  607. img.style.width = '100%'
  608. img.style.maxWidth = width + 'px'
  609. img.src = result
  610. if document.caretPositionFromPoint
  611. pos = document.caretPositionFromPoint(x, y)
  612. range = document.createRange()
  613. range.setStart(pos.offsetNode, pos.offset)
  614. range.collapse()
  615. range.insertNode(img)
  616. else if document.caretRangeFromPoint
  617. range = document.caretRangeFromPoint(x, y)
  618. range.insertNode(img)
  619. else
  620. console.log('could not find carat')
  621. # resize if to big
  622. @resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
  623. reader.readAsDataURL(file)
  624. onPaste: (e) =>
  625. e.stopPropagation()
  626. e.preventDefault()
  627. if e.clipboardData
  628. clipboardData = e.clipboardData
  629. else if window.clipboardData
  630. clipboardData = window.clipboardData
  631. else if e.clipboardData
  632. clipboardData = e.clipboardData
  633. else
  634. throw 'No clipboardData support'
  635. imageInserted = false
  636. if clipboardData && clipboardData.items && clipboardData.items[0]
  637. item = clipboardData.items[0]
  638. if item.kind == 'file' && (item.type == 'image/png' || item.type == 'image/jpeg')
  639. imageFile = item.getAsFile()
  640. reader = new FileReader()
  641. reader.onload = (e) =>
  642. insert = (dataUrl, width) =>
  643. # adapt image if we are on retina devices
  644. if @isRetina()
  645. width = width / 2
  646. img = new Image()
  647. img.style.width = '100%'
  648. img.style.maxWidth = width + 'px'
  649. img.src = dataUrl
  650. document.execCommand('insertHTML', false, img)
  651. # resize if to big
  652. @resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
  653. reader.readAsDataURL(imageFile)
  654. imageInserted = true
  655. return if imageInserted
  656. # check existing + paste text for limit
  657. text = undefined
  658. docType = undefined
  659. try
  660. text = clipboardData.getData('text/html')
  661. docType = 'html'
  662. if !text || text.length is 0
  663. docType = 'text'
  664. text = clipboardData.getData('text/plain')
  665. if !text || text.length is 0
  666. docType = 'text2'
  667. text = clipboardData.getData('text')
  668. catch e
  669. console.log('Sorry, can\'t insert markup because browser is not supporting it.')
  670. docType = 'text3'
  671. text = clipboardData.getData('text')
  672. if docType is 'text' || docType is 'text2' || docType is 'text3'
  673. text = '<div>' + text.replace(/\n/g, '</div><div>') + '</div>'
  674. text = text.replace(/<div><\/div>/g, '<div><br></div>')
  675. console.log('p', docType, text)
  676. if docType is 'html'
  677. html = document.createElement('div')
  678. # can't log because might contain malicious content
  679. # @log.debug 'HTML clipboard', text
  680. sanitized = DOMPurify.sanitize(text)
  681. @log.debug 'sanitized HTML clipboard', sanitized
  682. html.innerHTML = sanitized
  683. match = false
  684. htmlTmp = text
  685. regex = new RegExp('<(/w|w)\:[A-Za-z]')
  686. if htmlTmp.match(regex)
  687. match = true
  688. htmlTmp = htmlTmp.replace(regex, '')
  689. regex = new RegExp('<(/o|o)\:[A-Za-z]')
  690. if htmlTmp.match(regex)
  691. match = true
  692. htmlTmp = htmlTmp.replace(regex, '')
  693. if match
  694. html = @wordFilter(html)
  695. #html
  696. for node in html.childNodes
  697. if node.nodeType == 8
  698. node.remove()
  699. # remove tags, keep content
  700. for node in html.querySelectorAll('a, font, small, time, form, label')
  701. node.outerHTML = node.innerHTML
  702. # replace tags with generic div
  703. # New type of the tag
  704. replacementTag = 'div';
  705. # Replace all x tags with the type of replacementTag
  706. for node in html.querySelectorAll('textarea')
  707. outer = node.outerHTML
  708. # Replace opening tag
  709. regex = new RegExp('<' + node.tagName, 'i')
  710. newTag = outer.replace(regex, '<' + replacementTag)
  711. # Replace closing tag
  712. regex = new RegExp('</' + node.tagName, 'i')
  713. newTag = newTag.replace(regex, '</' + replacementTag)
  714. node.outerHTML = newTag
  715. # remove tags & content
  716. for node in html.querySelectorAll('font, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset')
  717. node.remove()
  718. @removeAttributes(html)
  719. text = html.innerHTML
  720. # as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower)
  721. if docType is 'text3'
  722. @pasteHtmlAtCaret(text)
  723. else
  724. document.execCommand('insertHTML', false, text)
  725. true
  726. onKeydown: (e) =>
  727. # check for enter
  728. if not @inputDisabled and not e.shiftKey and e.keyCode is 13
  729. e.preventDefault()
  730. @sendMessage()
  731. richtTextControl = false
  732. if !e.altKey && !e.ctrlKey && e.metaKey
  733. richtTextControl = true
  734. else if !e.altKey && e.ctrlKey && !e.metaKey
  735. richtTextControl = true
  736. if richtTextControl && @richTextFormatKey[ e.keyCode ]
  737. e.preventDefault()
  738. if e.keyCode is 66
  739. document.execCommand('bold')
  740. return true
  741. if e.keyCode is 73
  742. document.execCommand('italic')
  743. return true
  744. if e.keyCode is 85
  745. document.execCommand('underline')
  746. return true
  747. if e.keyCode is 83
  748. document.execCommand('strikeThrough')
  749. return true
  750. send: (event, data = {}) =>
  751. data.chat_id = @options.chatId
  752. @io.send(event, data)
  753. onWebSocketMessage: (pipes) =>
  754. for pipe in pipes
  755. @log.debug 'ws:onmessage', pipe
  756. switch pipe.event
  757. when 'chat_error'
  758. @log.notice pipe.data
  759. if pipe.data && pipe.data.state is 'chat_disabled'
  760. @destroy(remove: true)
  761. when 'chat_session_message'
  762. return if pipe.data.self_written
  763. @receiveMessage pipe.data
  764. when 'chat_session_typing'
  765. return if pipe.data.self_written
  766. @onAgentTypingStart()
  767. when 'chat_session_start'
  768. @onConnectionEstablished pipe.data
  769. when 'chat_session_queue'
  770. @onQueueScreen pipe.data
  771. when 'chat_session_closed'
  772. @onSessionClosed pipe.data
  773. when 'chat_session_left'
  774. @onSessionClosed pipe.data
  775. when 'chat_status_customer'
  776. switch pipe.data.state
  777. when 'online'
  778. @sessionId = undefined
  779. if !@options.cssAutoload || @cssLoaded
  780. @onReady()
  781. else
  782. @socketReady = true
  783. when 'offline'
  784. @onError 'Zammad Chat: No agent online'
  785. when 'chat_disabled'
  786. @onError 'Zammad Chat: Chat is disabled'
  787. when 'no_seats_available'
  788. @onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}"
  789. when 'reconnect'
  790. @onReopenSession pipe.data
  791. onReady: ->
  792. @log.debug 'widget ready for use'
  793. btn = document.querySelector(".#{ @options.buttonClass }")
  794. if btn
  795. btn.addEventListener('click', @open)
  796. btn.classList.remove(@options.inactiveClass)
  797. @options.onReady?()
  798. if @options.show
  799. @show()
  800. onError: (message) =>
  801. @log.debug message
  802. @addStatus(message)
  803. btn = document.querySelector(".#{ @options.buttonClass }")
  804. if btn
  805. btn.classList.add('zammad-chat-is-hidden')
  806. if @isOpen
  807. @disableInput()
  808. @destroy(remove: false)
  809. else
  810. @destroy(remove: true)
  811. @options.onError?(message)
  812. onReopenSession: (data) =>
  813. @log.debug 'old messages', data.session
  814. @inactiveTimeout.start()
  815. unfinishedMessage = sessionStorage.getItem 'unfinished_message'
  816. # rerender chat history
  817. if data.agent
  818. @onConnectionEstablished(data)
  819. for message in data.session
  820. @renderMessage
  821. message: message.content
  822. id: message.id
  823. from: if message.created_by_id then 'agent' else 'customer'
  824. if unfinishedMessage
  825. @input.innerHTML = unfinishedMessage
  826. # show wait list
  827. if data.position
  828. @onQueue data
  829. @show()
  830. @open()
  831. @scrollToBottom()
  832. if unfinishedMessage
  833. @input.focus()
  834. onInput: =>
  835. # remove unread-state from messages
  836. for message in @el.querySelectorAll('.zammad-chat-message--unread')
  837. message.classList.remove 'zammad-chat-message--unread'
  838. sessionStorage.setItem 'unfinished_message', @input.innerHTML
  839. @onTyping()
  840. onTyping: ->
  841. # send typing start event only every 1.5 seconds
  842. return if @isTyping && @isTyping > new Date(new Date().getTime() - 1500)
  843. @isTyping = new Date()
  844. @send 'chat_session_typing',
  845. session_id: @sessionId
  846. @inactiveTimeout.start()
  847. onSubmit: (event) =>
  848. event.preventDefault()
  849. @sendMessage()
  850. sendMessage: ->
  851. message = @input.innerHTML
  852. return if !message
  853. @inactiveTimeout.start()
  854. sessionStorage.removeItem 'unfinished_message'
  855. messageElement = @view('message')
  856. message: message
  857. from: 'customer'
  858. id: @_messageCount++
  859. unreadClass: ''
  860. @maybeAddTimestamp()
  861. # add message before message typing loader
  862. if @el.querySelector('.zammad-chat-message--typing')
  863. @lastAddedType = 'typing-placeholder'
  864. @el.querySelector('.zammad-chat-message--typing').insertAdjacentHTML('beforebegin', messageElement)
  865. else
  866. @lastAddedType = 'message--customer'
  867. @body.insertAdjacentHTML('beforeend', messageElement)
  868. @input.innerHTML = ''
  869. @scrollToBottom()
  870. # send message event
  871. @send 'chat_session_message',
  872. content: message
  873. id: @_messageCount
  874. session_id: @sessionId
  875. receiveMessage: (data) =>
  876. @inactiveTimeout.start()
  877. # hide writing indicator
  878. @onAgentTypingEnd()
  879. @maybeAddTimestamp()
  880. @renderMessage
  881. message: data.message.content
  882. id: data.id
  883. from: 'agent'
  884. @scrollToBottom showHint: true
  885. renderMessage: (data) =>
  886. @lastAddedType = "message--#{ data.from }"
  887. data.unreadClass = if document.hidden then ' zammad-chat-message--unread' else ''
  888. @body.insertAdjacentHTML('beforeend', @view('message')(data))
  889. open: =>
  890. if @isOpen
  891. @log.debug 'widget already open, block'
  892. return
  893. @isOpen = true
  894. @log.debug 'open widget'
  895. @show()
  896. if !@sessionId
  897. @showLoader()
  898. @el.classList.add 'zammad-chat-is-open'
  899. remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight
  900. @el.style.transform = "translateY(#{remainerHeight}px)"
  901. # force redraw
  902. @el.clientHeight
  903. if !@sessionId
  904. @el.addEventListener 'transitionend', @onOpenAnimationEnd
  905. @el.classList.add 'zammad-chat--animate'
  906. # force redraw
  907. @el.clientHeight
  908. # start animation
  909. @el.style.transform = ''
  910. @send('chat_session_init'
  911. url: window.location.href
  912. )
  913. else
  914. @el.style.transform = ''
  915. @onOpenAnimationEnd()
  916. onOpenAnimationEnd: =>
  917. @el.removeEventListener 'transitionend', @onOpenAnimationEnd
  918. @el.classList.remove 'zammad-chat--animate'
  919. @idleTimeout.stop()
  920. if @isFullscreen
  921. @disableScrollOnRoot()
  922. @options.onOpenAnimationEnd?()
  923. sessionClose: =>
  924. # send close
  925. @send 'chat_session_close',
  926. session_id: @sessionId
  927. # stop timer
  928. @inactiveTimeout.stop()
  929. @waitingListTimeout.stop()
  930. # delete input store
  931. sessionStorage.removeItem 'unfinished_message'
  932. # stop delay of initial queue position
  933. if @onInitialQueueDelayId
  934. clearTimeout(@onInitialQueueDelayId)
  935. @setSessionId undefined
  936. toggle: (event) =>
  937. if @isOpen
  938. @close(event)
  939. else
  940. @open(event)
  941. close: (event) =>
  942. if !@isOpen
  943. @log.debug 'can\'t close widget, it\'s not open'
  944. return
  945. if @initDelayId
  946. clearTimeout(@initDelayId)
  947. if @sessionId
  948. @log.debug 'session close before widget close'
  949. @sessionClose()
  950. @log.debug 'close widget'
  951. event.stopPropagation() if event
  952. if @isFullscreen
  953. @enableScrollOnRoot()
  954. # close window
  955. remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight
  956. @el.addEventListener 'transitionend', @onCloseAnimationEnd
  957. @el.classList.add 'zammad-chat--animate'
  958. # force redraw
  959. document.offsetHeight
  960. # animate out
  961. @el.style.transform = "translateY(#{remainerHeight}px)"
  962. onCloseAnimationEnd: =>
  963. @el.removeEventListener 'transitionend', @onCloseAnimationEnd
  964. @el.classList.remove 'zammad-chat-is-open', 'zammad-chat--animate'
  965. @el.style.transform = ''
  966. @showLoader()
  967. @el.querySelector('.zammad-chat-welcome').classList.remove('zammad-chat-is-hidden')
  968. @el.querySelector('.zammad-chat-agent').classList.add('zammad-chat-is-hidden')
  969. @el.querySelector('.zammad-chat-agent-status').classList.add('zammad-chat-is-hidden')
  970. @isOpen = false
  971. @options.onCloseAnimationEnd?()
  972. @io.reconnect()
  973. onWebSocketClose: =>
  974. return if @isOpen
  975. if @el
  976. @el.classList.remove('zammad-chat-is-shown')
  977. @el.classList.remove('zammad-chat-is-loaded')
  978. show: ->
  979. return if @state is 'offline'
  980. @el.classList.add('zammad-chat-is-loaded')
  981. @el.classList.add('zammad-chat-is-shown')
  982. disableInput: ->
  983. @inputDisabled = true
  984. @input.setAttribute('contenteditable', false)
  985. @el.querySelector('.zammad-chat-send').disabled = true
  986. @io.close()
  987. enableInput: ->
  988. @inputDisabled = false
  989. @input.setAttribute('contenteditable', true)
  990. @el.querySelector('.zammad-chat-send').disabled = false
  991. hideModal: ->
  992. @el.querySelector('.zammad-chat-modal').innerHTML = ''
  993. onQueueScreen: (data) =>
  994. @setSessionId data.session_id
  995. # delay initial queue position, show connecting first
  996. show = =>
  997. @onQueue data
  998. @waitingListTimeout.start()
  999. if @initialQueueDelay && !@onInitialQueueDelayId
  1000. @onInitialQueueDelayId = setTimeout(show, @initialQueueDelay)
  1001. return
  1002. # stop delay of initial queue position
  1003. if @onInitialQueueDelayId
  1004. clearTimeout(@onInitialQueueDelayId)
  1005. # show queue position
  1006. show()
  1007. onQueue: (data) =>
  1008. @log.notice 'onQueue', data.position
  1009. @inQueue = true
  1010. @el.querySelector('.zammad-chat-modal').innerHTML = @view('waiting')
  1011. position: data.position
  1012. onAgentTypingStart: =>
  1013. if @stopTypingId
  1014. clearTimeout(@stopTypingId)
  1015. @stopTypingId = setTimeout(@onAgentTypingEnd, 3000)
  1016. # never display two typing indicators
  1017. return if @el.querySelector('.zammad-chat-message--typing')
  1018. @maybeAddTimestamp()
  1019. @body.insertAdjacentHTML('beforeend', @view('typingIndicator')())
  1020. # only if typing indicator is shown
  1021. return if !@isVisible(@el.querySelector('.zammad-chat-message--typing'), true)
  1022. @scrollToBottom()
  1023. onAgentTypingEnd: =>
  1024. @el.querySelector('.zammad-chat-message--typing').remove() if @el.querySelector('.zammad-chat-message--typing')
  1025. onLeaveTemporary: =>
  1026. return if !@sessionId
  1027. @send 'chat_session_leave_temporary',
  1028. session_id: @sessionId
  1029. maybeAddTimestamp: ->
  1030. timestamp = Date.now()
  1031. if !@lastTimestamp or (timestamp - @lastTimestamp) > @showTimeEveryXMinutes * 60000
  1032. label = @T('Today')
  1033. time = new Date().toTimeString().substr 0,5
  1034. if @lastAddedType is 'timestamp'
  1035. # update last time
  1036. @updateLastTimestamp label, time
  1037. @lastTimestamp = timestamp
  1038. else
  1039. # add new timestamp
  1040. @body.insertAdjacentHTML 'beforeend', @view('timestamp')
  1041. label: label
  1042. time: time
  1043. @lastTimestamp = timestamp
  1044. @lastAddedType = 'timestamp'
  1045. @scrollToBottom()
  1046. updateLastTimestamp: (label, time) ->
  1047. return if !@el
  1048. timestamps = @el.querySelectorAll('.zammad-chat-body .zammad-chat-timestamp')
  1049. return if !timestamps
  1050. timestamps[timestamps.length - 1].outerHTML = @view('timestamp')
  1051. label: label
  1052. time: time
  1053. addStatus: (status) ->
  1054. return if !@el
  1055. @maybeAddTimestamp()
  1056. @body.insertAdjacentHTML 'beforeend', @view('status')
  1057. status: status
  1058. @scrollToBottom()
  1059. detectScrolledtoBottom: =>
  1060. scrollBottom = @body.scrollTop + @body.offsetHeight
  1061. @scrolledToBottom = Math.abs(scrollBottom - @body.scrollHeight) <= @scrollSnapTolerance
  1062. @el.querySelector('.zammad-scroll-hint').classList.add('is-hidden') if @scrolledToBottom
  1063. showScrollHint: ->
  1064. @el.querySelector('.zammad-scroll-hint').classList.remove('is-hidden')
  1065. # compensate scroll
  1066. @body.scrollTop = @body.scrollTop + @el.querySelector('.zammad-scroll-hint').offsetHeight
  1067. onScrollHintClick: =>
  1068. # animate scroll
  1069. @body.scrollTo
  1070. top: @body.scrollHeight
  1071. behavior: 'smooth'
  1072. scrollToBottom: ({ showHint } = { showHint: false }) ->
  1073. if @scrolledToBottom
  1074. @body.scrollTop = @body.scrollHeight
  1075. else if showHint
  1076. @showScrollHint()
  1077. destroy: (params = {}) =>
  1078. @log.debug 'destroy widget', params
  1079. @setAgentOnlineState 'offline'
  1080. if params.remove && @el
  1081. @el.remove()
  1082. # Remove button, because it can no longer be used.
  1083. btn = document.querySelector(".#{ @options.buttonClass }")
  1084. if btn
  1085. btn.classList.add @options.inactiveClass
  1086. btn.style.display = 'none';
  1087. # stop all timer
  1088. if @waitingListTimeout
  1089. @waitingListTimeout.stop()
  1090. if @inactiveTimeout
  1091. @inactiveTimeout.stop()
  1092. if @idleTimeout
  1093. @idleTimeout.stop()
  1094. # stop ws connection
  1095. @io.close()
  1096. reconnect: =>
  1097. # set status to connecting
  1098. @log.notice 'reconnecting'
  1099. @disableInput()
  1100. @lastAddedType = 'status'
  1101. @setAgentOnlineState 'connecting'
  1102. @addStatus @T('Connection lost')
  1103. onConnectionReestablished: =>
  1104. # set status back to online
  1105. @lastAddedType = 'status'
  1106. @setAgentOnlineState 'online'
  1107. @addStatus @T('Connection re-established')
  1108. @options.onConnectionReestablished?()
  1109. onSessionClosed: (data) ->
  1110. @addStatus @T('Chat closed by %s', data.realname)
  1111. @disableInput()
  1112. @setAgentOnlineState 'offline'
  1113. @inactiveTimeout.stop()
  1114. @options.onSessionClosed?(data)
  1115. setSessionId: (id) =>
  1116. @sessionId = id
  1117. if id is undefined
  1118. sessionStorage.removeItem 'sessionId'
  1119. else
  1120. sessionStorage.setItem 'sessionId', id
  1121. onConnectionEstablished: (data) =>
  1122. # stop delay of initial queue position
  1123. if @onInitialQueueDelayId
  1124. clearTimeout @onInitialQueueDelayId
  1125. @inQueue = false
  1126. if data.agent
  1127. @agent = data.agent
  1128. if data.session_id
  1129. @setSessionId data.session_id
  1130. # empty old messages
  1131. @body.innerHTML = ''
  1132. @el.querySelector('.zammad-chat-agent').innerHTML = @view('agent')
  1133. agent: @agent
  1134. @enableInput()
  1135. @hideModal()
  1136. @el.querySelector('.zammad-chat-welcome').classList.add('zammad-chat-is-hidden')
  1137. @el.querySelector('.zammad-chat-agent').classList.remove('zammad-chat-is-hidden')
  1138. @el.querySelector('.zammad-chat-agent-status').classList.remove('zammad-chat-is-hidden')
  1139. @input.focus() if not @isFullscreen
  1140. @setAgentOnlineState 'online'
  1141. @waitingListTimeout.stop()
  1142. @idleTimeout.stop()
  1143. @inactiveTimeout.start()
  1144. @options.onConnectionEstablished?(data)
  1145. showCustomerTimeout: ->
  1146. @el.querySelector('.zammad-chat-modal').innerHTML = @view('customer_timeout')
  1147. agent: @agent.name
  1148. delay: @options.inactiveTimeout
  1149. @el.querySelector('.js-restart').addEventListener 'click', -> location.reload()
  1150. @sessionClose()
  1151. showWaitingListTimeout: ->
  1152. @el.querySelector('.zammad-chat-modal').innerHTML = @view('waiting_list_timeout')
  1153. delay: @options.watingListTimeout
  1154. @el.querySelector('.js-restart').addEventListener 'click', -> location.reload()
  1155. @sessionClose()
  1156. showLoader: ->
  1157. @el.querySelector('.zammad-chat-modal').innerHTML = @view('loader')()
  1158. setAgentOnlineState: (state) =>
  1159. @state = state
  1160. return if !@el
  1161. capitalizedState = state.charAt(0).toUpperCase() + state.slice(1)
  1162. @el.querySelector('.zammad-chat-agent-status').dataset.status = state
  1163. @el.querySelector('.zammad-chat-agent-status').textContent = @T(capitalizedState)
  1164. detectHost: ->
  1165. protocol = 'ws://'
  1166. if scriptProtocol is 'https'
  1167. protocol = 'wss://'
  1168. @options.host = "#{ protocol }#{ scriptHost }/ws"
  1169. loadCss: ->
  1170. return if !@options.cssAutoload
  1171. url = @options.cssUrl
  1172. if !url
  1173. url = @options.host
  1174. .replace(/^wss/i, 'https')
  1175. .replace(/^ws/i, 'http')
  1176. .replace(/\/ws$/i, '') # WebSocket may run on example.com/ws path
  1177. url += '/assets/chat/chat.css'
  1178. @log.debug "load css from '#{url}'"
  1179. styles = "@import url('#{url}');"
  1180. newSS = document.createElement('link')
  1181. newSS.onload = @onCssLoaded
  1182. newSS.rel = 'stylesheet'
  1183. newSS.href = 'data:text/css,' + escape(styles)
  1184. document.getElementsByTagName('head')[0].appendChild(newSS)
  1185. onCssLoaded: =>
  1186. @cssLoaded = true
  1187. if @socketReady
  1188. @onReady()
  1189. @options.onCssLoaded?()
  1190. startTimeoutObservers: =>
  1191. @idleTimeout = new Timeout(
  1192. logPrefix: 'idleTimeout'
  1193. debug: @options.debug
  1194. timeout: @options.idleTimeout
  1195. timeoutIntervallCheck: @options.idleTimeoutIntervallCheck
  1196. callback: =>
  1197. @log.debug 'Idle timeout reached, hide widget', new Date
  1198. @destroy(remove: true)
  1199. )
  1200. @inactiveTimeout = new Timeout(
  1201. logPrefix: 'inactiveTimeout'
  1202. debug: @options.debug
  1203. timeout: @options.inactiveTimeout
  1204. timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck
  1205. callback: =>
  1206. @log.debug 'Inactive timeout reached, show timeout screen.', new Date
  1207. @showCustomerTimeout()
  1208. @destroy(remove: false)
  1209. )
  1210. @waitingListTimeout = new Timeout(
  1211. logPrefix: 'waitingListTimeout'
  1212. debug: @options.debug
  1213. timeout: @options.waitingListTimeout
  1214. timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck
  1215. callback: =>
  1216. @log.debug 'Waiting list timeout reached, show timeout screen.', new Date
  1217. @showWaitingListTimeout()
  1218. @destroy(remove: false)
  1219. )
  1220. disableScrollOnRoot: ->
  1221. @rootScrollOffset = @scrollRoot.scrollTop
  1222. @scrollRoot.style.overflow = 'hidden'
  1223. @scrollRoot.style.position = 'fixed'
  1224. enableScrollOnRoot: ->
  1225. @scrollRoot.scrollTop = @rootScrollOffset
  1226. @scrollRoot.style.overflow = ''
  1227. @scrollRoot.style.position = ''
  1228. # based on https://github.com/customd/jquery-visible/blob/master/jquery.visible.js
  1229. # to have not dependency, port to coffeescript
  1230. isVisible: (el, partial, hidden, direction) ->
  1231. return if el.length < 1
  1232. vpWidth = window.innerWidth
  1233. vpHeight = window.innerHeight
  1234. direction = if direction then direction else 'both'
  1235. clientSize = if hidden is true then t.offsetWidth * t.offsetHeight else true
  1236. rec = el.getBoundingClientRect()
  1237. tViz = rec.top >= 0 && rec.top < vpHeight
  1238. bViz = rec.bottom > 0 && rec.bottom <= vpHeight
  1239. lViz = rec.left >= 0 && rec.left < vpWidth
  1240. rViz = rec.right > 0 && rec.right <= vpWidth
  1241. vVisible = if partial then tViz || bViz else tViz && bViz
  1242. hVisible = if partial then lViz || rViz else lViz && rViz
  1243. if direction is 'both'
  1244. return clientSize && vVisible && hVisible
  1245. else if direction is 'vertical'
  1246. return clientSize && vVisible
  1247. else if direction is 'horizontal'
  1248. return clientSize && hVisible
  1249. isRetina: ->
  1250. if window.matchMedia
  1251. 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)')
  1252. return (mq && mq.matches || (window.devicePixelRatio > 1))
  1253. false
  1254. resizeImage: (dataURL, x = 'auto', y = 'auto', sizeFactor = 1, type, quallity, callback, force = true) ->
  1255. # load image from data url
  1256. imageObject = new Image()
  1257. imageObject.onload = ->
  1258. imageWidth = imageObject.width
  1259. imageHeight = imageObject.height
  1260. console.log('ImageService', 'current size', imageWidth, imageHeight)
  1261. if y is 'auto' && x is 'auto'
  1262. x = imageWidth
  1263. y = imageHeight
  1264. # get auto dimensions
  1265. if y is 'auto'
  1266. factor = imageWidth / x
  1267. y = imageHeight / factor
  1268. if x is 'auto'
  1269. factor = imageWidth / y
  1270. x = imageHeight / factor
  1271. # check if resize is needed
  1272. resize = false
  1273. if x < imageWidth || y < imageHeight
  1274. resize = true
  1275. x = x * sizeFactor
  1276. y = y * sizeFactor
  1277. else
  1278. x = imageWidth
  1279. y = imageHeight
  1280. # create canvas and set dimensions
  1281. canvas = document.createElement('canvas')
  1282. canvas.width = x
  1283. canvas.height = y
  1284. # draw image on canvas and set image dimensions
  1285. context = canvas.getContext('2d')
  1286. context.drawImage(imageObject, 0, 0, x, y)
  1287. # set quallity based on image size
  1288. if quallity == 'auto'
  1289. if x < 200 && y < 200
  1290. quallity = 1
  1291. else if x < 400 && y < 400
  1292. quallity = 0.9
  1293. else if x < 600 && y < 600
  1294. quallity = 0.8
  1295. else if x < 900 && y < 900
  1296. quallity = 0.7
  1297. else
  1298. quallity = 0.6
  1299. # execute callback with resized image
  1300. newDataUrl = canvas.toDataURL(type, quallity)
  1301. if resize
  1302. console.log('ImageService', 'resize', x/sizeFactor, y/sizeFactor, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
  1303. callback(newDataUrl, x/sizeFactor, y/sizeFactor, true)
  1304. return
  1305. console.log('ImageService', 'no resize', x, y, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
  1306. callback(newDataUrl, x, y, false)
  1307. # load image from data url
  1308. imageObject.src = dataURL
  1309. # taken from https://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
  1310. pasteHtmlAtCaret: (html) ->
  1311. sel = undefined
  1312. range = undefined
  1313. if window.getSelection
  1314. sel = window.getSelection()
  1315. if sel.getRangeAt && sel.rangeCount
  1316. range = sel.getRangeAt(0)
  1317. range.deleteContents()
  1318. el = document.createElement('div')
  1319. el.innerHTML = html
  1320. frag = document.createDocumentFragment(node, lastNode)
  1321. while node = el.firstChild
  1322. lastNode = frag.appendChild(node)
  1323. range.insertNode(frag)
  1324. if lastNode
  1325. range = range.cloneRange()
  1326. range.setStartAfter(lastNode)
  1327. range.collapse(true)
  1328. sel.removeAllRanges()
  1329. sel.addRange(range)
  1330. else if document.selection && document.selection.type != 'Control'
  1331. document.selection.createRange().pasteHTML(html)
  1332. # (C) sbrin - https://github.com/sbrin
  1333. # https://gist.github.com/sbrin/6801034
  1334. wordFilter: (editor) ->
  1335. content = editor.html()
  1336. # Word comments like conditional comments etc
  1337. content = content.replace(/<!--[\s\S]+?-->/gi, '')
  1338. # Remove comments, scripts (e.g., msoShowComment), XML tag, VML content,
  1339. # MS Office namespaced tags, and a few other tags
  1340. content = content.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, '')
  1341. # Convert <s> into <strike> for line-though
  1342. content = content.replace(/<(\/?)s>/gi, '<$1strike>')
  1343. # Replace nbsp entites to char since it's easier to handle
  1344. # content = content.replace(/&nbsp;/gi, "\u00a0")
  1345. content = content.replace(/&nbsp;/gi, ' ')
  1346. # Convert <span style="mso-spacerun:yes">___</span> to string of alternating
  1347. # breaking/non-breaking spaces of same length
  1348. #content = content.replace(/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi, (str, spaces) ->
  1349. # return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : ''
  1350. #)
  1351. editor.innerHTML = content
  1352. # Parse out list indent level for lists
  1353. for p in editor.querySelectorAll('p')
  1354. str = p.getAttribute('style')
  1355. matches = /mso-list:\w+ \w+([0-9]+)/.exec(str)
  1356. if matches
  1357. p.dataset._listLevel = parseInt(matches[1], 10)
  1358. # Parse Lists
  1359. last_level = 0
  1360. pnt = null
  1361. for p in editor.querySelectorAll('p')
  1362. cur_level = p.dataset._listLevel
  1363. if cur_level != undefined
  1364. txt = p.textContent
  1365. list_tag = '<ul></ul>'
  1366. if (/^\s*\w+\./.test(txt))
  1367. matches = /([0-9])\./.exec(txt)
  1368. if matches
  1369. start = parseInt(matches[1], 10)
  1370. list_tag = start>1 ? '<ol start="' + start + '"></ol>' : '<ol></ol>'
  1371. else
  1372. list_tag = '<ol></ol>'
  1373. if cur_level > last_level
  1374. if last_level == 0
  1375. p.insertAdjacentHTML 'beforebegin', list_tag
  1376. pnt = p.previousElementSibling
  1377. else
  1378. pnt.insertAdjacentHTML 'beforeend', list_tag
  1379. if cur_level < last_level
  1380. for i in [i..last_level-cur_level]
  1381. pnt = pnt.parentNode
  1382. p.querySelector('span:first').remove() if p.querySelector('span:first')
  1383. pnt.insertAdjacentHTML 'beforeend', '<li>' + p.innerHTML + '</li>'
  1384. p.remove()
  1385. last_level = cur_level
  1386. else
  1387. last_level = 0
  1388. el.removeAttribute('style') for el in editor.querySelectorAll('[style]')
  1389. el.removeAttribute('align') for el in editor.querySelectorAll('[align]')
  1390. el.outerHTML = el.innerHTML for el in editor.querySelectorAll('span')
  1391. el.remove() for el in editor.querySelectorAll('span:empty')
  1392. el.removeAttribute('class') for el in editor.querySelectorAll("[class^='Mso']")
  1393. el.remove() for el in editor.querySelectorAll('p:empty')
  1394. editor
  1395. removeAttribute: (element) ->
  1396. return if !element
  1397. for att in element.attributes
  1398. element.removeAttribute(att.name)
  1399. removeAttributes: (html) =>
  1400. for node in html.querySelectorAll('*')
  1401. @removeAttribute node
  1402. html
  1403. window.ZammadChat = ZammadChat