trabajo-afectivo/public/assets/chat/chat-no-jquery.coffee

1486 lines
55 KiB
CoffeeScript
Raw Normal View History

2019-01-22 06:12:32 +00:00
do(window) ->
scripts = document.getElementsByTagName('script')
# search for script to get protocol and hostname for ws connection
myScript = scripts[scripts.length - 1]
scriptProtocol = window.location.protocol.replace(':', '') # set default protocol
if myScript && myScript.src
scriptHost = myScript.src.match('.*://([^:/]*).*')[1]
scriptProtocol = myScript.src.match('(.*)://[^:/]*.*')[1]
# Define the plugin class
class Core
defaults:
debug: false
constructor: (options) ->
@options = {}
for key, value of @defaults
@options[key] = value
for key, value of options
@options[key] = value
class Base extends Core
constructor: (options) ->
super(options)
@log = new Log(debug: @options.debug, logPrefix: @options.logPrefix || @logPrefix)
class Log extends Core
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
element = document.querySelector('.js-chatLogDisplay')
if element
element.innerHTML = '<div>' + logString + '</div>' + element.innerHTML
class Timeout extends Base
timeoutStartedAt: null
logPrefix: 'timeout'
defaults:
debug: false
timeout: 4
timeoutIntervallCheck: 0.5
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'
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)
@ping()
@ws.onmessage = (e) =>
pipes = JSON.parse(e.data)
@log.debug 'onMessage', e.data
for pipe in pipes
if pipe.event is 'pong'
@ping()
if @options.onMessage
@options.onMessage(pipes)
@ws.onclose = (e) =>
@log.debug 'close websocket connection', e
if @pingDelayId
clearTimeout(@pingDelayId)
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
ping: =>
localPing = =>
@send('ping')
@pingDelayId = setTimeout(localPing, 29000)
class ZammadChat extends Base
defaults:
chatId: undefined
show: true
target: document.querySelector('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!'
scrollHint: 'Scroll down to see new messages'
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: 2
lastTimestamp: null
lastAddedType: null
inputTimeout: null
isTyping: false
state: 'offline'
initialQueueDelay: 10000
translations:
'da':
'<strong>Chat</strong> with us!': '<strong>Chat</strong> med os!'
'Scroll down to see new messages': 'Scroll ned for at se nye beskeder'
'Online': 'Online'
'Offline': 'Offline'
'Connecting': 'Forbinder'
'Connection re-established': 'Forbindelse genoprettet'
'Today': 'I dag'
'Send': 'Send'
'Chat closed by %s': 'Chat lukket af %s'
'Compose your message...': 'Skriv en besked...'
'All colleagues are busy.': 'Alle kollegaer er optaget.'
'You are on waiting list position <strong>%s</strong>.': 'Du er i venteliste som nummer <strong>%s</strong>.'
'Start new conversation': 'Start en ny samtale'
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Da du ikke har svaret i de sidste %s minutter er din samtale med <strong>%s</strong> blevet lukket.'
'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Da du ikke har svaret i de sidste %s minutter er din samtale blevet lukket.'
'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Vi beklager, det tager længere end forventet at få en ledig plads. Prøv venligst igen senere eller send os en e-mail. På forhånd tak!'
2019-01-22 06:12:32 +00:00
'de':
'<strong>Chat</strong> with us!': '<strong>Chatte</strong> mit uns!'
'Scroll down to see new messages': 'Scrolle nach unten um neue Nachrichten zu sehen'
'Online': 'Online'
'Offline': 'Offline'
'Connecting': 'Verbinden'
'Connection re-established': 'Verbindung wiederhergestellt'
'Today': 'Heute'
'Send': 'Senden'
'Chat closed by %s': 'Chat beendet von %s'
'Compose your message...': 'Ihre Nachricht...'
'All colleagues 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.'
'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!'
'es':
'<strong>Chat</strong> with us!': '<strong>Chatee</strong> con nosotros!'
'Scroll down to see new messages': 'Haga scroll hacia abajo para ver nuevos mensajes'
'Online': 'En linea'
'Offline': 'Desconectado'
'Connecting': 'Conectando'
'Connection re-established': 'Conexión restablecida'
'Today': 'Hoy'
'Send': 'Enviar'
'Chat closed by %s': 'Chat cerrado por %s'
'Compose your message...': 'Escriba su mensaje...'
'All colleagues are busy.': 'Todos los agentes están ocupados.'
'You are on waiting list position <strong>%s</strong>.': 'Usted está en la posición <strong>%s</strong> de la lista de espera.'
'Start new conversation': 'Iniciar nueva conversación'
'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.'
'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.'
'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!'
'fr':
'<strong>Chat</strong> with us!': '<strong>Chattez</strong> avec nous!'
'Scroll down to see new messages': 'Faites défiler pour lire les nouveaux messages'
'Online': 'En-ligne'
'Offline': 'Hors-ligne'
'Connecting': 'Connexion en cours'
'Connection re-established': 'Connexion rétablie'
'Today': 'Aujourdhui'
'Send': 'Envoyer'
'Chat closed by %s': 'Chat fermé par %s'
'Compose your message...': 'Composez votre message...'
'All colleagues are busy.': 'Tous les collègues sont actuellement occupés.'
'You are on waiting list position <strong>%s</strong>.': 'Vous êtes actuellement en <strong>%s</strong> position dans la file d\'attente.'
'Start new conversation': 'Démarrer une nouvelle conversation'
'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.'
'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.'
'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!'
'nl':
'<strong>Chat</strong> with us!': '<strong>Chat</strong> met ons!'
'Scroll down to see new messages': 'Scrol naar beneden om nieuwe berichten te zien'
'Online': 'Online'
'Offline': 'Offline'
'Connecting': 'Verbinden'
'Connection re-established': 'Verbinding herstelt'
'Today': 'Vandaag'
'Send': 'Verzenden'
'Chat closed by %s': 'Chat gesloten door %s'
'Compose your message...': 'Typ uw bericht...'
'All colleagues are busy.': 'Alle medewerkers zijn bezet.'
'You are on waiting list position <strong>%s</strong>.': 'U bent <strong>%s</strong> in de wachtrij.'
'Start new conversation': 'Nieuwe conversatie starten'
'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.'
'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.'
'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!'
'it':
'<strong>Chat</strong> with us!': '<strong>Chatta</strong> con noi!'
'Scroll down to see new messages': 'Scorrere verso il basso per vedere i nuovi messaggi'
'Online': 'Online'
'Offline': 'Offline'
'Connecting': 'Collegamento'
'Connection re-established': 'Collegamento ristabilito'
'Today': 'Oggi'
'Send': 'Invio'
'Chat closed by %s': 'Conversazione chiusa da %s'
'Compose your message...': 'Comporre il tuo messaggio...'
'All colleagues are busy.': 'Tutti i colleghi sono occupati.'
'You are on waiting list position <strong>%s</strong>.': 'Siete in posizione lista d\' attesa <strong>%s</strong>.'
'Start new conversation': 'Avviare una nuova conversazione'
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione con <strong>%s</strong> si è chiusa.'
'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione si è chiusa.'
'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Ci dispiace, ci vuole più tempo come previsto per ottenere uno slot vuoto. Per favore riprova più tardi o inviaci un\' e-mail. Grazie!'
'pl':
'<strong>Chat</strong> with us!': '<strong>Czatuj</strong> z nami!'
'Scroll down to see new messages': 'Przewiń w dół, aby wyświetlić nowe wiadomości'
'Online': 'Online'
'Offline': 'Offline'
'Connecting': 'Łączenie'
'Connection re-established': 'Ponowne nawiązanie połączenia'
'Today': 'dzisiejszy'
'Send': 'Wyślij'
'Chat closed by %s': 'Czat zamknięty przez %s'
'Compose your message...': 'Utwórz swoją wiadomość...'
'All colleagues are busy.': 'Wszyscy koledzy są zajęci.'
'You are on waiting list position <strong>%s</strong>.': 'Na liście oczekujących znajduje się pozycja <strong>%s</strong>.'
'Start new conversation': 'Rozpoczęcie nowej konwersacji'
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z <strong>%s</strong> została zamknięta.'
'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.'
'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!'
'zh-cn':
'<strong>Chat</strong> with us!': '发起<strong>即时对话</strong>!'
'Scroll down to see new messages': '向下滚动以查看新消息'
'Online': '在线'
'Offline': '离线'
'Connecting': '连接中'
'Connection re-established': '正在重新建立连接'
'Today': '今天'
'Send': '发送'
'Chat closed by %s': 'Chat closed by %s'
'Compose your message...': '正在输入信息...'
'All colleagues are busy.': '所有工作人员都在忙碌中.'
'You are on waiting list position <strong>%s</strong>.': '您目前的等候位置是第 <strong>%s</strong> 位.'
'Start new conversation': '开始新的会话'
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': '由于您超过 %s 分钟没有回复, 您与 <strong>%s</strong> 的会话已被关闭.'
'Since you didn\'t respond in the last %s minutes your conversation got closed.': '由于您超过 %s 分钟没有任何回复, 该对话已被关闭.'
'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': '非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!'
'zh-tw':
'<strong>Chat</strong> with us!': '開始<strong>即時對话</strong>!'
'Scroll down to see new messages': '向下滑動以查看新訊息'
'Online': '線上'
'Offline': '离线'
'Connecting': '連線中'
'Connection re-established': '正在重新建立連線中'
'Today': '今天'
'Send': '發送'
'Chat closed by %s': 'Chat closed by %s'
'Compose your message...': '正在輸入訊息...'
'All colleagues are busy.': '所有服務人員都在忙碌中.'
'You are on waiting list position <strong>%s</strong>.': '你目前的等候位置是第 <strong>%s</strong> 順位.'
'Start new conversation': '開始新的對話'
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': '由於你超過 %s 分鐘沒有回應, 你與 <strong>%s</strong> 的對話已被關閉.'
'Since you didn\'t respond in the last %s minutes your conversation got closed.': '由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.'
'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': '非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!'
'ru':
'<strong>Chat</strong> with us!': 'Напишите нам!'
'Scroll down to see new messages': 'Прокрутите, чтобы увидеть новые сообщения'
'Online': 'Онлайн'
'Offline': 'Оффлайн'
'Connecting': 'Подключение'
'Connection re-established': 'Подключение восстановлено'
'Today': 'Сегодня'
'Send': 'Отправить'
'Chat closed by %s': '%s закрыл чат'
'Compose your message...': 'Напишите сообщение...'
'All colleagues are busy.': 'Все сотрудники заняты'
'You are on waiting list position %s.': 'Вы в списке ожидания под номером %s'
'Start new conversation': 'Начать новую переписку.'
'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.'
'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.'
'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!'
sessionId: undefined
scrolledToBottom: true
scrollSnapTolerance: 10
richTextFormatKey:
66: true # b
73: true # i
85: true # u
83: true # s
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) ->
super(options)
# jQuery migration
if typeof jQuery != 'undefined' && @options.target instanceof jQuery
@log.notice 'Chat: target option is a jQuery object. jQuery is not a requirement for the chat any more.'
@options.target = @options.target.get(0)
# fullscreen
@isFullscreen = (window.matchMedia and window.matchMedia('(max-width: 768px)').matches)
@scrollRoot = @getScrollRoot()
# check prerequisites
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 = document.documentElement.getAttribute('lang')
if @options.lang
if !@translations[@options.lang]
@log.debug "lang: No #{@options.lang} found, try first two letters"
@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()
getScrollRoot: ->
return document.scrollingElement if 'scrollingElement' of document
html = document.documentElement
start = parseInt(html.pageYOffset, 10)
html.pageYOffset = start + 1
end = parseInt(html.pageYOffset, 10)
html.pageYOffset = start
return if end > start then html else document.body
render: =>
if !@el || !document.querySelector('.zammad-chat')
@renderBase()
# disable open button
document.querySelector(".#{ @options.buttonClass }").classList.add @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
url: window.location.href
renderBase: ->
@el.remove() if @el
@options.target.innerHTML += @view('chat')(
title: @options.title,
scrollHint: @options.scrollHint
)
@el = @options.target.querySelector('.zammad-chat')
@input = @el.querySelector('.zammad-chat-input')
@body = @el.querySelector('.zammad-chat-body')
# start bindings
@el.querySelector('.js-chat-open').addEventListener('click', @open)
@el.querySelector('.js-chat-toggle').addEventListener('click', @toggle)
@el.querySelector('.js-chat-status').addEventListener('click', @stopPropagation)
@el.querySelector('.zammad-chat-controls').addEventListener('submit', @onSubmit)
@body.addEventListener('scroll', @detectScrolledtoBottom)
@el.querySelector('.zammad-scroll-hint').addEventListener('click', @onScrollHintClick)
@input.addEventListener('keydown', @onKeydown)
@input.addEventListener('input', @onInput)
@input.addEventListener('paste', @onPaste)
@input.addEventListener('drop', @onDrop)
window.addEventListener('beforeunload', @onLeaveTemporary)
window.addEventListener('hashchange', =>
if @isOpen
if @sessionId
@send 'chat_session_notice',
session_id: @sessionId
message: window.location.href
return
@idleTimeout.start()
)
stopPropagation: (event) ->
event.stopPropagation()
onPaste: (e) =>
e.stopPropagation()
e.preventDefault()
if window.dataTransfer # ie
dataTransfer = window.dataTransfer
else if e.dataTransfer # other browsers
dataTransfer = e.dataTransfer
else
throw 'No clipboardData support'
x = e.clientX
y = e.clientY
file = dataTransfer.files[0]
# look for images
if file.type.match('image.*')
reader = new FileReader()
reader.onload = (e) =>
# Insert the image at the carat
insert = (dataUrl, width) =>
# adapt image if we are on retina devices
if @isRetina()
width = width / 2
result = dataUrl
img = new Image()
img.style.width = '100%'
img.style.maxWidth = width +'px'
img.src = result
if document.caretPositionFromPoint
pos = document.caretPositionFromPoint(x, y)
range = document.createRange()
range.setStart(pos.offsetNode, pos.offset)
range.collapse()
range.insertNode(img)
else if document.caretRangeFromPoint
range = document.caretRangeFromPoint(x, y)
range.insertNode(img)
else
console.log('could not find carat')
# resize if to big
@resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
reader.readAsDataURL(file)
onPaste: (e) =>
e.stopPropagation()
e.preventDefault()
if e.clipboardData
clipboardData = e.clipboardData
else if window.clipboardData
clipboardData = window.clipboardData
else if e.clipboardData
clipboardData = e.clipboardData
else
throw 'No clipboardData support'
imageInserted = false
if clipboardData && clipboardData.items && clipboardData.items[0]
item = clipboardData.items[0]
if item.kind == 'file' && (item.type == 'image/png' || item.type == 'image/jpeg')
imageFile = item.getAsFile()
reader = new FileReader()
reader.onload = (e) =>
insert = (dataUrl, width) =>
# adapt image if we are on retina devices
if @isRetina()
width = width / 2
img = new Image()
img.style.width = '100%'
img.style.maxWidth = width +'px'
img.src = dataUrl
document.execCommand('insertHTML', false, img)
# resize if to big
@resizeImage(e.target.result, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
reader.readAsDataURL(imageFile)
imageInserted = true
return if imageInserted
# check existing + paste text for limit
text = undefined
docType = undefined
try
text = clipboardData.getData('text/html')
docType = 'html'
if !text || text.length is 0
docType = 'text'
text = clipboardData.getData('text/plain')
if !text || text.length is 0
docType = 'text2'
text = clipboardData.getData('text')
catch e
console.log('Sorry, can\'t insert markup because browser is not supporting it.')
docType = 'text3'
text = clipboardData.getData('text')
if docType is 'text' || docType is 'text2' || docType is 'text3'
text = '<div>' + text.replace(/\n/g, '</div><div>') + '</div>'
text = text.replace(/<div><\/div>/g, '<div><br></div>')
console.log('p', docType, text)
if docType is 'html'
html = document.createElement('div')
html.innerHTML = text
match = false
htmlTmp = text
regex = new RegExp('<(/w|w)\:[A-Za-z]')
if htmlTmp.match(regex)
match = true
htmlTmp = htmlTmp.replace(regex, '')
regex = new RegExp('<(/o|o)\:[A-Za-z]')
if htmlTmp.match(regex)
match = true
htmlTmp = htmlTmp.replace(regex, '')
if match
html = @wordFilter(html)
#html
for node in html.childNodes
if node.nodeType == 8
node.remove()
# remove tags, keep content
for node in html.querySelectorAll('a, font, small, time, form, label')
node.outerHTML = node.innerHTML
# replace tags with generic div
# New type of the tag
replacementTag = 'div';
# Replace all x tags with the type of replacementTag
for node in html.querySelectorAll('textarea')
outer = node.outerHTML
# Replace opening tag
regex = new RegExp('<' + node.tagName, 'i')
newTag = outer.replace(regex, '<' + replacementTag)
# Replace closing tag
regex = new RegExp('</' + node.tagName, 'i')
newTag = newTag.replace(regex, '</' + replacementTag)
node.outerHTML = newTag
# remove tags & content
for node in html.querySelectorAll('font, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset')
node.remove()
@removeAttributes(html)
text = html.innerHTML
# as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower)
if docType is 'text3'
@pasteHtmlAtCaret(text)
else
document.execCommand('insertHTML', false, text)
true
onKeydown: (e) =>
# check for enter
if not e.shiftKey and e.keyCode is 13
e.preventDefault()
@sendMessage()
richtTextControl = false
if !e.altKey && !e.ctrlKey && e.metaKey
richtTextControl = true
else if !e.altKey && e.ctrlKey && !e.metaKey
richtTextControl = true
if richtTextControl && @richTextFormatKey[ e.keyCode ]
e.preventDefault()
if e.keyCode is 66
document.execCommand('bold')
return true
if e.keyCode is 73
document.execCommand('italic')
return true
if e.keyCode is 85
document.execCommand('underline')
return true
if e.keyCode is 83
document.execCommand('strikeThrough')
return true
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
if !@options.cssAutoload || @cssLoaded
@onReady()
else
@socketReady = true
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'
btn = document.querySelector(".#{ @options.buttonClass }")
if btn
btn.addEventListener('click', @open)
btn.classList.remove(@inactiveClass)
if @options.show
@show()
onError: (message) =>
@log.debug message
@addStatus(message)
document.querySelector(".#{ @options.buttonClass }").classList.add('zammad-chat-is-hidden')
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.innerHTML = unfinishedMessage
# show wait list
if data.position
@onQueue data
@show()
@open()
@scrollToBottom()
if unfinishedMessage
@input.focus()
onInput: =>
# remove unread-state from messages
for message in @el.querySelectorAll('.zammad-chat-message--unread')
node.classList.remove 'zammad-chat-message--unread'
sessionStorage.setItem 'unfinished_message', @input.innerHTML
@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.innerHTML
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.querySelector('.zammad-chat-message--typing')
@lastAddedType = 'typing-placeholder'
@el.querySelector('.zammad-chat-message--typing').insertAdjacentHTML('beforebegin', messageElement)
else
@lastAddedType = 'message--customer'
@body.insertAdjacentHTML('beforeend', messageElement)
@input.innerHTML = ''
@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'
@scrollToBottom showHint: true
renderMessage: (data) =>
@lastAddedType = "message--#{ data.from }"
data.unreadClass = if document.hidden then ' zammad-chat-message--unread' else ''
@body.insertAdjacentHTML('beforeend', @view('message')(data))
open: =>
if @isOpen
@log.debug 'widget already open, block'
return
@isOpen = true
@log.debug 'open widget'
@show()
if !@sessionId
@showLoader()
@el.classList.add 'zammad-chat-is-open'
remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight
@el.style.transform = "translateY(#{remainerHeight}px)"
# force redraw
@el.clientHeight
if !@sessionId
@el.addEventListener 'transitionend', @onOpenTransitionend
@el.classList.add 'zammad-chat--animate'
# force redraw
@el.clientHeight
# start animation
@el.style.transform = ''
@send('chat_session_init'
url: window.location.href
)
else
@el.style.transform = ''
@onOpenTransitionend()
onOpenTransitionend: =>
@el.removeEventListener 'transitionend', @onOpenTransitionend
@el.classList.remove 'zammad-chat--animate'
@idleTimeout.stop()
if @isFullscreen
@disableScrollOnRoot()
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
toggle: (event) =>
if @isOpen
@close(event)
else
@open(event)
close: (event) =>
if !@isOpen
@log.debug 'can\'t close widget, it\'s not open'
return
if @initDelayId
clearTimeout(@initDelayId)
if !@sessionId
@log.debug 'can\'t close widget without sessionId'
return
@log.debug 'close widget'
event.stopPropagation() if event
@sessionClose()
if @isFullscreen
@enableScrollOnRoot()
# close window
remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight
@el.addEventListener 'transitionend', @onCloseTransitionend
@el.classList.add 'zammad-chat--animate'
# force redraw
document.offsetHeight
# animate out
@el.style.transform = "translateY(#{remainerHeight}px)"
onCloseTransitionend: =>
@el.removeEventListener 'transitionend', @onCloseTransitionend
@el.classList.remove 'zammad-chat-is-open', 'zammad-chat--animate'
@el.style.transform = ''
@showLoader()
@el.querySelector('.zammad-chat-welcome').classList.remove('zammad-chat-is-hidden')
@el.querySelector('.zammad-chat-agent').classList.add('zammad-chat-is-hidden')
@el.querySelector('.zammad-chat-agent-status').classList.add('zammad-chat-is-hidden')
@isOpen = false
@io.reconnect()
onWebSocketClose: =>
return if @isOpen
if @el
@el.classList.remove('zammad-chat-is-shown')
@el.classList.remove('zammad-chat-is-loaded')
show: ->
return if @state is 'offline'
@el.classList.add('zammad-chat-is-loaded')
@el.classList.add('zammad-chat-is-shown')
disableInput: ->
@input.disabled = true
@el.querySelector('.zammad-chat-send').disabled = true
enableInput: ->
@input.disabled = false
@el.querySelector('.zammad-chat-send').disabled = false
hideModal: ->
@el.querySelector('.zammad-chat-modal').innerHTML = ''
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.querySelector('.zammad-chat-modal').innerHTML = @view('waiting')
position: data.position
onAgentTypingStart: =>
if @stopTypingId
clearTimeout(@stopTypingId)
@stopTypingId = setTimeout(@onAgentTypingEnd, 3000)
# never display two typing indicators
return if @el.querySelector('.zammad-chat-message--typing')
@maybeAddTimestamp()
@body.insertAdjacentHTML('beforeend', @view('typingIndicator')())
# only if typing indicator is shown
return if !@isVisible(@el.querySelector('.zammad-chat-message--typing'), true)
@scrollToBottom()
onAgentTypingEnd: =>
@el.querySelector('.zammad-chat-message--typing').remove() if @el.querySelector('.zammad-chat-message--typing')
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
@body.insertAdjacentHTML 'beforeend', @view('timestamp')
label: label
time: time
@lastTimestamp = timestamp
@lastAddedType = 'timestamp'
@scrollToBottom()
updateLastTimestamp: (label, time) ->
return if !@el
timestamps = @el.querySelectorAll('.zammad-chat-body .zammad-chat-timestamp')
return if !timestamps
timestamps[timestamps.length - 1].outerHTML = @view('timestamp')
label: label
time: time
addStatus: (status) ->
return if !@el
@maybeAddTimestamp()
@body.insertAdjacentHTML 'beforeend', @view('status')
status: status
@scrollToBottom()
detectScrolledtoBottom: =>
scrollBottom = @body.scrollTop + @body.offsetHeight
@scrolledToBottom = Math.abs(scrollBottom - @body.scrollHeight) <= @scrollSnapTolerance
@el.querySelector('.zammad-scroll-hint').classList.add('is-hidden') if @scrolledToBottom
showScrollHint: ->
@el.querySelector('.zammad-scroll-hint').classList.remove('is-hidden')
# compensate scroll
@body.scrollTop = @body.scrollTop + @el.querySelector('.zammad-scroll-hint').offsetHeight
onScrollHintClick: =>
# animate scroll
@body.scrollTo
top: @body.scrollHeight
behavior: 'smooth'
scrollToBottom: ({ showHint } = { showHint: false }) ->
if @scrolledToBottom
@body.scrollTop = @body.scrollHeight
else if showHint
@showScrollHint()
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
# empty old messages
@body.innerHTML = ''
@el.querySelector('.zammad-chat-agent').innerHTML = @view('agent')
agent: @agent
@enableInput()
@hideModal()
@el.querySelector('.zammad-chat-welcome').classList.add('zammad-chat-is-hidden')
@el.querySelector('.zammad-chat-agent').classList.remove('zammad-chat-is-hidden')
@el.querySelector('.zammad-chat-agent-status').classList.remove('zammad-chat-is-hidden')
@input.focus() if not @isFullscreen
@setAgentOnlineState 'online'
@waitingListTimeout.stop()
@idleTimeout.stop()
@inactiveTimeout.start()
showCustomerTimeout: ->
@el.querySelector('.zammad-chat-modal').innerHTML = @view('customer_timeout')
agent: @agent.name
delay: @options.inactiveTimeout
@el.querySelector('.js-restart').addEventListener 'click', -> location.reload()
@sessionClose()
showWaitingListTimeout: ->
@el.querySelector('.zammad-chat-modal').innerHTML = @view('waiting_list_timeout')
delay: @options.watingListTimeout
@el.querySelector('.js-restart').addEventListener 'click', -> location.reload()
@sessionClose()
showLoader: ->
@el.querySelector('.zammad-chat-modal').innerHTML = @view('loader')()
setAgentOnlineState: (state) =>
@state = state
return if !@el
capitalizedState = state.charAt(0).toUpperCase() + state.slice(1)
@el.querySelector('.zammad-chat-agent-status').dataset.status = state
@el.querySelector('.zammad-chat-agent-status').textContent = @T(capitalizedState)
detectHost: ->
protocol = 'ws://'
if scriptProtocol 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.onload = @onCssLoaded
newSS.rel = 'stylesheet'
newSS.href = 'data:text/css,' + escape(styles)
document.getElementsByTagName('head')[0].appendChild(newSS)
onCssLoaded: =>
@cssLoaded = true
if @socketReady
@onReady()
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)
)
disableScrollOnRoot: ->
@rootScrollOffset = @scrollRoot.scrollTop
@scrollRoot.style.overflow = 'hidden'
@scrollRoot.style.position = 'fixed'
enableScrollOnRoot: ->
@scrollRoot.scrollTop = @rootScrollOffset +'px'
@scrollRoot.style.overflow = ''
@scrollRoot.style.position = ''
# based on https://github.com/customd/jquery-visible/blob/master/jquery.visible.js
# to have not dependency, port to coffeescript
isVisible: (el, partial, hidden, direction) ->
return if el.length < 1
vpWidth = window.innerWidth
vpHeight = window.innerHeight
direction = if direction then direction else 'both'
clientSize = if hidden is true then t.offsetWidth * t.offsetHeight else true
rec = el.getBoundingClientRect()
tViz = rec.top >= 0 && rec.top < vpHeight
bViz = rec.bottom > 0 && rec.bottom <= vpHeight
lViz = rec.left >= 0 && rec.left < vpWidth
rViz = rec.right > 0 && rec.right <= vpWidth
vVisible = if partial then tViz || bViz else tViz && bViz
hVisible = if partial then lViz || rViz else lViz && rViz
if direction is 'both'
return clientSize && vVisible && hVisible
else if direction is 'vertical'
return clientSize && vVisible
else if direction is 'horizontal'
return clientSize && hVisible
isRetina: ->
if window.matchMedia
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)')
return (mq && mq.matches || (window.devicePixelRatio > 1))
false
resizeImage: (dataURL, x = 'auto', y = 'auto', sizeFactor = 1, type, quallity, callback, force = true) ->
# load image from data url
imageObject = new Image()
imageObject.onload = ->
imageWidth = imageObject.width
imageHeight = imageObject.height
console.log('ImageService', 'current size', imageWidth, imageHeight)
if y is 'auto' && x is 'auto'
x = imageWidth
y = imageHeight
# get auto dimensions
if y is 'auto'
factor = imageWidth / x
y = imageHeight / factor
if x is 'auto'
factor = imageWidth / y
x = imageHeight / factor
# check if resize is needed
resize = false
if x < imageWidth || y < imageHeight
resize = true
x = x * sizeFactor
y = y * sizeFactor
else
x = imageWidth
y = imageHeight
# create canvas and set dimensions
canvas = document.createElement('canvas')
canvas.width = x
canvas.height = y
# draw image on canvas and set image dimensions
context = canvas.getContext('2d')
context.drawImage(imageObject, 0, 0, x, y)
# set quallity based on image size
if quallity == 'auto'
if x < 200 && y < 200
quallity = 1
else if x < 400 && y < 400
quallity = 0.9
else if x < 600 && y < 600
quallity = 0.8
else if x < 900 && y < 900
quallity = 0.7
else
quallity = 0.6
# execute callback with resized image
newDataUrl = canvas.toDataURL(type, quallity)
if resize
console.log('ImageService', 'resize', x/sizeFactor, y/sizeFactor, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
callback(newDataUrl, x/sizeFactor, y/sizeFactor, true)
return
console.log('ImageService', 'no resize', x, y, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
callback(newDataUrl, x, y, false)
# load image from data url
imageObject.src = dataURL
# taken from https://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
pasteHtmlAtCaret: (html) ->
sel = undefined
range = undefined
if window.getSelection
sel = window.getSelection()
if sel.getRangeAt && sel.rangeCount
range = sel.getRangeAt(0)
range.deleteContents()
el = document.createElement('div')
el.innerHTML = html
frag = document.createDocumentFragment(node, lastNode)
while node = el.firstChild
lastNode = frag.appendChild(node)
range.insertNode(frag)
if lastNode
range = range.cloneRange()
range.setStartAfter(lastNode)
range.collapse(true)
sel.removeAllRanges()
sel.addRange(range)
else if document.selection && document.selection.type != 'Control'
document.selection.createRange().pasteHTML(html)
# (C) sbrin - https://github.com/sbrin
# https://gist.github.com/sbrin/6801034
wordFilter: (editor) ->
content = editor.html()
# Word comments like conditional comments etc
content = content.replace(/<!--[\s\S]+?-->/gi, '')
# Remove comments, scripts (e.g., msoShowComment), XML tag, VML content,
# MS Office namespaced tags, and a few other tags
content = content.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, '')
# Convert <s> into <strike> for line-though
content = content.replace(/<(\/?)s>/gi, '<$1strike>')
# Replace nbsp entites to char since it's easier to handle
# content = content.replace(/&nbsp;/gi, "\u00a0")
content = content.replace(/&nbsp;/gi, ' ')
# Convert <span style="mso-spacerun:yes">___</span> to string of alternating
# breaking/non-breaking spaces of same length
#content = content.replace(/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi, (str, spaces) ->
# return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : ''
#)
editor.innerHTML = content
# Parse out list indent level for lists
for p in editor.querySelectorAll('p')
str = p.getAttribute('style')
matches = /mso-list:\w+ \w+([0-9]+)/.exec(str)
if matches
p.dataset._listLevel = parseInt(matches[1], 10)
# Parse Lists
last_level = 0
pnt = null
for p in editor.querySelectorAll('p')
cur_level = p.dataset._listLevel
if cur_level != undefined
txt = p.textContent
list_tag = '<ul></ul>'
if (/^\s*\w+\./.test(txt))
matches = /([0-9])\./.exec(txt)
if matches
start = parseInt(matches[1], 10)
list_tag = start>1 ? '<ol start="' + start + '"></ol>' : '<ol></ol>'
else
list_tag = '<ol></ol>'
if cur_level > last_level
if last_level == 0
p.insertAdjacentHTML 'beforebegin', list_tag
pnt = p.previousElementSibling
else
pnt.insertAdjacentHTML 'beforeend', list_tag
if cur_level < last_level
for i in [i..last_level-cur_level]
pnt = pnt.parentNode
p.querySelector('span:first').remove() if p.querySelector('span:first')
pnt.insertAdjacentHTML 'beforeend', '<li>' + p.innerHTML + '</li>'
p.remove()
last_level = cur_level
else
last_level = 0
el.removeAttribute('style') for el in editor.querySelectorAll('[style]')
el.removeAttribute('align') for el in editor.querySelectorAll('[align]')
el.outerHTML = el.innerHTML for el in editor.querySelectorAll('span')
el.remove() for el in editor.querySelectorAll('span:empty')
el.removeAttribute('class') for el in editor.querySelectorAll("[class^='Mso']")
el.remove() for el in editor.querySelectorAll('p:empty')
editor
removeAttribute: (element) ->
return if !element
for att in element.attributes
element.removeAttribute(att.name)
removeAttributes: (html) =>
for node in html.querySelectorAll('*')
@removeAttribute node
html
window.ZammadChat = ZammadChat