123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800 |
- do($ = window.jQuery, window) ->
- scripts = document.getElementsByTagName('script')
- myScript = scripts[scripts.length - 1]
- scriptHost = myScript.src.match('.*://([^:/]*).*')[1]
- # Define the plugin class
- class Base
- defaults:
- debug: false
- constructor: (options) ->
- @options = $.extend {}, @defaults, options
- @log = new Log(debug: @options.debug, logPrefix: @options.logPrefix || @logPrefix)
- class Log
- defaults:
- debug: false
- constructor: (options) ->
- @options = $.extend {}, @defaults, options
- debug: (items...) =>
- return if !@options.debug
- @log('debug', items)
- notice: (items...) =>
- @log('notice', items)
- error: (items...) =>
- @log('error', items)
- log: (level, items) =>
- items.unshift('||')
- items.unshift(level)
- items.unshift(@options.logPrefix)
- console.log.apply console, items
- return if !@options.debug
- logString = ''
- for item in items
- logString += ' '
- if typeof item is 'object'
- logString += JSON.stringify(item)
- else if item && item.toString
- logString += item.toString()
- else
- logString += item
- $('.js-chatLogDisplay').prepend('<div>' + logString + '</div>')
- class Timeout extends Base
- timeoutStartedAt: null
- logPrefix: 'timeout'
- defaults:
- debug: false
- timeout: 4
- timeoutIntervallCheck: 0.5
- constructor: (options) ->
- super(options)
- start: =>
- @stop()
- timeoutStartedAt = new Date
- check = =>
- timeLeft = new Date - new Date(timeoutStartedAt.getTime() + @options.timeout * 1000 * 60)
- @log.debug "Timeout check for #{@options.timeout} minutes (left #{timeLeft/1000} sec.)"#, new Date
- return if timeLeft < 0
- @stop()
- @options.callback()
- @log.debug "Start timeout in #{@options.timeout} minutes"#, new Date
- @intervallId = setInterval(check, @options.timeoutIntervallCheck * 1000 * 60)
- stop: =>
- return if !@intervallId
- @log.debug "Stop timeout of #{@options.timeout} minutes"#, new Date
- clearInterval(@intervallId)
- class Io extends Base
- logPrefix: 'io'
- constructor: (options) ->
- super(options)
- set: (params) =>
- for key, value of params
- @options[key] = value
- connect: =>
- @log.debug "Connecting to #{@options.host}"
- @ws = new window.WebSocket("#{@options.host}")
- @ws.onopen = (e) =>
- @log.debug 'onOpen', e
- @options.onOpen(e)
- @ws.onmessage = (e) =>
- pipes = JSON.parse(e.data)
- @log.debug 'onMessage', e.data
- if @options.onMessage
- @options.onMessage(pipes)
- @ws.onclose = (e) =>
- @log.debug 'close websocket connection', e
- if @manualClose
- @log.debug 'manual close, onClose callback'
- @manualClose = false
- if @options.onClose
- @options.onClose(e)
- else
- @log.debug 'error close, onError callback'
- if @options.onError
- @options.onError('Connection lost...')
- @ws.onerror = (e) =>
- @log.debug 'onError', e
- if @options.onError
- @options.onError(e)
- close: =>
- @log.debug 'close websocket manually'
- @manualClose = true
- @ws.close()
- reconnect: =>
- @log.debug 'reconnect'
- @close()
- @connect()
- send: (event, data = {}) =>
- @log.debug 'send', event, data
- msg = JSON.stringify
- event: event
- data: data
- @ws.send msg
- class ZammadChat extends Base
- defaults:
- chatId: undefined
- show: true
- target: $('body')
- host: ''
- debug: false
- flat: false
- lang: undefined
- cssAutoload: true
- cssUrl: undefined
- fontSize: undefined
- buttonClass: 'open-zammad-chat'
- inactiveClass: 'is-inactive'
- title: '<strong>Chat</strong> with us!'
- idleTimeout: 6
- idleTimeoutIntervallCheck: 0.5
- inactiveTimeout: 8
- inactiveTimeoutIntervallCheck: 0.5
- waitingListTimeout: 4
- waitingListTimeoutIntervallCheck: 0.5
- logPrefix: 'chat'
- _messageCount: 0
- isOpen: false
- blinkOnlineInterval: null
- stopBlinOnlineStateTimeout: null
- showTimeEveryXMinutes: 1
- lastTimestamp: null
- lastAddedType: null
- inputTimeout: null
- isTyping: false
- state: 'offline'
- initialQueueDelay: 10000
- translations:
- de:
- '<strong>Chat</strong> with us!': '<strong>Chat</strong> mit uns!'
- 'Online': 'Online'
- 'Online': 'Online'
- 'Offline': 'Offline'
- 'Connecting': 'Verbinden'
- 'Connection re-established': 'Verbindung wiederhergestellt'
- 'Today': 'Heute'
- 'Send': 'Senden'
- 'Compose your message...': 'Ihre Nachricht...'
- 'All colleges are busy.': 'Alle Kollegen sind belegt.'
- 'You are on waiting list position <strong>%s</strong>.': 'Sie sind in der Warteliste an der Position <strong>%s</strong>.'
- 'Start new conversation': 'Neue Konversation starten'
- '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.'
- '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.'
- sessionId: undefined
- T: (string, items...) =>
- if @options.lang && @options.lang isnt 'en'
- if !@translations[@options.lang]
- @log.notice "Translation '#{@options.lang}' needed!"
- else
- translations = @translations[@options.lang]
- if !translations[string]
- @log.notice "Translation needed for '#{string}'"
- string = translations[string] || string
- if items
- for item in items
- string = string.replace(/%s/, item)
- string
- view: (name) =>
- return (options) =>
- if !options
- options = {}
- options.T = @T
- options.background = @options.background
- options.flat = @options.flat
- options.fontSize = @options.fontSize
- return window.zammadChatTemplates[name](options)
- constructor: (options) ->
- @options = $.extend {}, @defaults, options
- super(@options)
- # check prerequisites
- if !$
- @state = 'unsupported'
- @log.notice 'Chat: no jquery found!'
- return
- if !window.WebSocket or !sessionStorage
- @state = 'unsupported'
- @log.notice 'Chat: Browser not supported!'
- return
- if !@options.chatId
- @state = 'unsupported'
- @log.error 'Chat: need chatId as option!'
- return
- # detect language
- if !@options.lang
- @options.lang = $('html').attr('lang')
- if @options.lang
- @options.lang = @options.lang.replace(/-.+?$/, '') # replace "-xx" of xx-xx
- @log.debug "lang: #{@options.lang}"
- # detect host
- @detectHost() if !@options.host
- @loadCss()
- @io = new Io(@options)
- @io.set(
- onOpen: @render
- onClose: @onWebSocketClose
- onMessage: @onWebSocketMessage
- onError: @onError
- )
- @io.connect()
- render: =>
- if !@el || !$('.zammad-chat').get(0)
- @el = $(@view('chat')(
- title: @options.title
- ))
- @options.target.append @el
- @input = @el.find('.zammad-chat-input')
- # start bindings
- @el.find('.js-chat-open').click @open
- @el.find('.js-chat-close').click @close
- @el.find('.zammad-chat-controls').on 'submit', @onSubmit
- @input.on
- keydown: @checkForEnter
- input: @onInput
- $(window).on('beforeunload', =>
- @onLeaveTemporary()
- )
- $(window).bind('hashchange', =>
- return if @isOpen
- @idleTimeout.start()
- )
- # disable open button
- $(".#{ @options.buttonClass }").addClass @inactiveClass
- @setAgentOnlineState 'online'
- @log.debug 'widget rendered'
- @startTimeoutObservers()
- @idleTimeout.start()
- # get current chat status
- @sessionId = sessionStorage.getItem('sessionId')
- @send 'chat_status_customer',
- session_id: @sessionId
- checkForEnter: (event) =>
- if not event.shiftKey and event.keyCode is 13
- event.preventDefault()
- @sendMessage()
- send: (event, data = {}) =>
- data.chat_id = @options.chatId
- @io.send(event, data)
- onWebSocketMessage: (pipes) =>
- for pipe in pipes
- @log.debug 'ws:onmessage', pipe
- switch pipe.event
- when 'chat_error'
- @log.notice pipe.data
- if pipe.data && pipe.data.state is 'chat_disabled'
- @destroy(remove: true)
- when 'chat_session_message'
- return if pipe.data.self_written
- @receiveMessage pipe.data
- when 'chat_session_typing'
- return if pipe.data.self_written
- @onAgentTypingStart()
- when 'chat_session_start'
- @onConnectionEstablished pipe.data
- when 'chat_session_queue'
- @onQueueScreen pipe.data
- when 'chat_session_closed'
- @onSessionClosed pipe.data
- when 'chat_session_left'
- @onSessionClosed pipe.data
- when 'chat_status_customer'
- switch pipe.data.state
- when 'online'
- @sessionId = undefined
- @onReady()
- when 'offline'
- @onError 'Zammad Chat: No agent online'
- when 'chat_disabled'
- @onError 'Zammad Chat: Chat is disabled'
- when 'no_seats_available'
- @onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}"
- when 'reconnect'
- @onReopenSession pipe.data
- onReady: ->
- @log.debug 'widget ready for use'
- $(".#{ @options.buttonClass }").click(@open).removeClass(@inactiveClass)
- if @options.show
- @show()
- onError: (message) =>
- @log.debug message
- @addStatus(message)
- $(".#{ @options.buttonClass }").hide()
- if @isOpen
- @disableInput()
- @destroy(remove: false)
- else
- @destroy(remove: true)
- onReopenSession: (data) =>
- @log.debug 'old messages', data.session
- @inactiveTimeout.start()
- unfinishedMessage = sessionStorage.getItem 'unfinished_message'
- # rerender chat history
- if data.agent
- @onConnectionEstablished(data)
- for message in data.session
- @renderMessage
- message: message.content
- id: message.id
- from: if message.created_by_id then 'agent' else 'customer'
- if unfinishedMessage
- @input.val unfinishedMessage
- # show wait list
- if data.position
- @onQueue data
- @show()
- @open()
- @scrollToBottom()
- if unfinishedMessage
- @input.focus()
- onInput: =>
- # remove unread-state from messages
- @el.find('.zammad-chat-message--unread')
- .removeClass 'zammad-chat-message--unread'
- sessionStorage.setItem 'unfinished_message', @input.val()
- @onTyping()
- onTyping: ->
- # send typing start event only every 1.5 seconds
- return if @isTyping && @isTyping > new Date(new Date().getTime() - 1500)
- @isTyping = new Date()
- @send 'chat_session_typing',
- session_id: @sessionId
- @inactiveTimeout.start()
- onSubmit: (event) =>
- event.preventDefault()
- @sendMessage()
- sendMessage: ->
- message = @input.val()
- return if !message
- @inactiveTimeout.start()
- sessionStorage.removeItem 'unfinished_message'
- messageElement = @view('message')
- message: message
- from: 'customer'
- id: @_messageCount++
- unreadClass: ''
- @maybeAddTimestamp()
- # add message before message typing loader
- if @el.find('.zammad-chat-message--typing').size()
- @lastAddedType = 'typing-placeholder'
- @el.find('.zammad-chat-message--typing').before messageElement
- else
- @lastAddedType = 'message--customer'
- @el.find('.zammad-chat-body').append messageElement
- @input.val('')
- @scrollToBottom()
- # send message event
- @send 'chat_session_message',
- content: message
- id: @_messageCount
- session_id: @sessionId
- receiveMessage: (data) =>
- @inactiveTimeout.start()
- # hide writing indicator
- @onAgentTypingEnd()
- @maybeAddTimestamp()
- @renderMessage
- message: data.message.content
- id: data.id
- from: 'agent'
- renderMessage: (data) =>
- @lastAddedType = "message--#{ data.from }"
- data.unreadClass = if document.hidden then ' zammad-chat-message--unread' else ''
- @el.find('.zammad-chat-body').append @view('message')(data)
- @scrollToBottom()
- open: =>
- if @isOpen
- @log.debug 'widget already open, block'
- return
- @isOpen = true
- @log.debug 'open widget'
- if !@sessionId
- @showLoader()
- @el.addClass('zammad-chat-is-open')
- if !@sessionId
- @el.animate { bottom: 0 }, 500, @onOpenAnimationEnd
- @send('chat_session_init')
- else
- @el.css 'bottom', 0
- @onOpenAnimationEnd()
- onOpenAnimationEnd: =>
- @idleTimeout.stop()
- sessionClose: =>
- # send close
- @send 'chat_session_close',
- session_id: @sessionId
- # stop timer
- @inactiveTimeout.stop()
- @waitingListTimeout.stop()
- # delete input store
- sessionStorage.removeItem 'unfinished_message'
- # stop delay of initial queue position
- if @onInitialQueueDelayId
- clearTimeout(@onInitialQueueDelayId)
- @setSessionId undefined
- close: (event) =>
- if !@isOpen
- @log.debug 'can\'t close widget, it\'s not open'
- return
- if !@sessionId
- @log.debug 'can\'t close widget without sessionId'
- return
- @log.debug 'close widget'
- event.stopPropagation() if event
- @sessionClose()
- # close window
- @el.removeClass('zammad-chat-is-open')
- remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
- @el.animate { bottom: -remainerHeight }, 500, @onCloseAnimationEnd
- onCloseAnimationEnd: =>
- @el.removeClass('zammad-chat-is-visible')
- @showLoader()
- @el.find('.zammad-chat-welcome').removeClass('zammad-chat-is-hidden')
- @el.find('.zammad-chat-agent').addClass('zammad-chat-is-hidden')
- @el.find('.zammad-chat-agent-status').addClass('zammad-chat-is-hidden')
- @isOpen = false
- @io.reconnect()
- onWebSocketClose: =>
- return if @isOpen
- if @el
- @el.removeClass('zammad-chat-is-shown')
- show: ->
- return if @state is 'offline'
- @el.addClass('zammad-chat-is-shown')
- if !@inputInitialized
- @inputInitialized = true
- @input.autoGrow
- extraLine: false
- remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
- @el.css 'bottom', -remainerHeight
- disableInput: ->
- @input.prop('disabled', true)
- @el.find('.zammad-chat-send').prop('disabled', true)
- enableInput: ->
- @input.prop('disabled', false)
- @el.find('.zammad-chat-send').prop('disabled', false)
- onQueueScreen: (data) =>
- @setSessionId data.session_id
- # delay initial queue position, show connecting first
- show = =>
- @onQueue data
- @waitingListTimeout.start()
- if @initialQueueDelay && !@onInitialQueueDelayId
- @onInitialQueueDelayId = setTimeout(show, @initialQueueDelay)
- return
- # stop delay of initial queue position
- if @onInitialQueueDelayId
- clearTimeout(@onInitialQueueDelayId)
- # show queue position
- show()
- onQueue: (data) =>
- @log.notice 'onQueue', data.position
- @inQueue = true
- @el.find('.zammad-chat-body').html @view('waiting')
- position: data.position
- onAgentTypingStart: =>
- if @stopTypingId
- clearTimeout(@stopTypingId)
- @stopTypingId = setTimeout(@onAgentTypingEnd, 3000)
- # never display two typing indicators
- return if @el.find('.zammad-chat-message--typing').size()
- @maybeAddTimestamp()
- @el.find('.zammad-chat-body').append @view('typingIndicator')()
- @scrollToBottom()
- onAgentTypingEnd: =>
- @el.find('.zammad-chat-message--typing').remove()
- onLeaveTemporary: =>
- return if !@sessionId
- @send 'chat_session_leave_temporary',
- session_id: @sessionId
- maybeAddTimestamp: ->
- timestamp = Date.now()
- if !@lastTimestamp or (timestamp - @lastTimestamp) > @showTimeEveryXMinutes * 60000
- label = @T('Today')
- time = new Date().toTimeString().substr 0,5
- if @lastAddedType is 'timestamp'
- # update last time
- @updateLastTimestamp label, time
- @lastTimestamp = timestamp
- else
- # add new timestamp
- @el.find('.zammad-chat-body').append @view('timestamp')
- label: label
- time: time
- @lastTimestamp = timestamp
- @lastAddedType = 'timestamp'
- @scrollToBottom()
- updateLastTimestamp: (label, time) ->
- return if !@el
- @el.find('.zammad-chat-body')
- .find('.zammad-chat-timestamp')
- .last()
- .replaceWith @view('timestamp')
- label: label
- time: time
- addStatus: (status) ->
- return if !@el
- @maybeAddTimestamp()
- @el.find('.zammad-chat-body').append @view('status')
- status: status
- @scrollToBottom()
- scrollToBottom: ->
- @el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight'))
- destroy: (params = {}) =>
- @log.debug 'destroy widget', params
- @setAgentOnlineState 'offline'
- if params.remove && @el
- @el.remove()
- # stop all timer
- if @waitingListTimeout
- @waitingListTimeout.stop()
- if @inactiveTimeout
- @inactiveTimeout.stop()
- if @idleTimeout
- @idleTimeout.stop()
- # stop ws connection
- @io.close()
- reconnect: =>
- # set status to connecting
- @log.notice 'reconnecting'
- @disableInput()
- @lastAddedType = 'status'
- @setAgentOnlineState 'connecting'
- @addStatus @T('Connection lost')
- onConnectionReestablished: =>
- # set status back to online
- @lastAddedType = 'status'
- @setAgentOnlineState 'online'
- @addStatus @T('Connection re-established')
- onSessionClosed: (data) ->
- @addStatus @T('Chat closed by %s', data.realname)
- @disableInput()
- @setAgentOnlineState 'offline'
- @inactiveTimeout.stop()
- setSessionId: (id) =>
- @sessionId = id
- if id is undefined
- sessionStorage.removeItem 'sessionId'
- else
- sessionStorage.setItem 'sessionId', id
- onConnectionEstablished: (data) =>
- # stop delay of initial queue position
- if @onInitialQueueDelayId
- clearTimeout @onInitialQueueDelayId
- @inQueue = false
- if data.agent
- @agent = data.agent
- if data.session_id
- @setSessionId data.session_id
- @el.find('.zammad-chat-agent').html @view('agent')
- agent: @agent
- @enableInput()
- @el.find('.zammad-chat-body').empty()
- @el.find('.zammad-chat-welcome').addClass('zammad-chat-is-hidden')
- @el.find('.zammad-chat-agent').removeClass('zammad-chat-is-hidden')
- @el.find('.zammad-chat-agent-status').removeClass('zammad-chat-is-hidden')
- @input.focus()
- @setAgentOnlineState 'online'
- @waitingListTimeout.stop()
- @idleTimeout.stop()
- @inactiveTimeout.start()
- showCustomerTimeout: ->
- @el.find('.zammad-chat-body').html @view('customer_timeout')
- agent: @agent.name
- delay: @options.inactiveTimeout
- reload = ->
- location.reload()
- @el.find('.js-restart').click reload
- @sessionClose()
- showWaitingListTimeout: ->
- @el.find('.zammad-chat-body').html @view('waiting_list_timeout')
- delay: @options.watingListTimeout
- reload = ->
- location.reload()
- @el.find('.js-restart').click reload
- @sessionClose()
- showLoader: ->
- @el.find('.zammad-chat-body').html @view('loader')()
- setAgentOnlineState: (state) =>
- @state = state
- return if !@el
- capitalizedState = state.charAt(0).toUpperCase() + state.slice(1)
- @el
- .find('.zammad-chat-agent-status')
- .attr('data-status', state)
- .text @T(capitalizedState)
- detectHost: ->
- protocol = 'ws://'
- if window.location.protocol is 'https:'
- protocol = 'wss://'
- @options.host = "#{ protocol }#{ scriptHost }/ws"
- loadCss: ->
- return if !@options.cssAutoload
- url = @options.cssUrl
- if !url
- url = @options.host
- .replace(/^wss/i, 'https')
- .replace(/^ws/i, 'http')
- .replace(/\/ws/i, '')
- url += '/assets/chat/chat.css'
- @log.debug "load css from '#{url}'"
- styles = "@import url('#{url}');"
- newSS = document.createElement('link')
- newSS.rel = 'stylesheet'
- newSS.href = 'data:text/css,' + escape(styles)
- document.getElementsByTagName('head')[0].appendChild(newSS)
- startTimeoutObservers: =>
- @idleTimeout = new Timeout(
- logPrefix: 'idleTimeout'
- debug: @options.debug
- timeout: @options.idleTimeout
- timeoutIntervallCheck: @options.idleTimeoutIntervallCheck
- callback: =>
- @log.debug 'Idle timeout reached, hide widget', new Date
- @destroy(remove: true)
- )
- @inactiveTimeout = new Timeout(
- logPrefix: 'inactiveTimeout'
- debug: @options.debug
- timeout: @options.inactiveTimeout
- timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck
- callback: =>
- @log.debug 'Inactive timeout reached, show timeout screen.', new Date
- @showCustomerTimeout()
- @destroy(remove: false)
- )
- @waitingListTimeout = new Timeout(
- logPrefix: 'waitingListTimeout'
- debug: @options.debug
- timeout: @options.waitingListTimeout
- timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck
- callback: =>
- @log.debug 'Waiting list timeout reached, show timeout screen.', new Date
- @showWaitingListTimeout()
- @destroy(remove: false)
- )
- window.ZammadChat = ZammadChat
|