chat.coffee 49 KB

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