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>/g, '
')
console.log('p', docType, text)
if docType is 'html'
html = document.createElement('div')
# can't log because might contain malicious content
# @log.debug 'HTML clipboard', text
sanitized = DOMPurify.sanitize(text)
@log.debug 'sanitized HTML clipboard', sanitized
html.innerHTML = sanitized
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 @inputDisabled and 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(@options.inactiveClass)
@options.onReady?()
if @options.show
@show()
onError: (message) =>
@log.debug message
@addStatus(message)
btn = document.querySelector(".#{ @options.buttonClass }")
if btn
btn.classList.add('zammad-chat-is-hidden')
if @isOpen
@disableInput()
@destroy(remove: false)
else
@destroy(remove: true)
@options.onError?(message)
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')
message.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', @onOpenAnimationEnd
@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 = ''
@onOpenAnimationEnd()
onOpenAnimationEnd: =>
@el.removeEventListener 'transitionend', @onOpenAnimationEnd
@el.classList.remove 'zammad-chat--animate'
@idleTimeout.stop()
if @isFullscreen
@disableScrollOnRoot()
@options.onOpenAnimationEnd?()
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 'session close before widget close'
@sessionClose()
@log.debug 'close widget'
event.stopPropagation() if event
if @isFullscreen
@enableScrollOnRoot()
# close window
remainerHeight = @el.clientHeight - @el.querySelector('.zammad-chat-header').offsetHeight
@el.addEventListener 'transitionend', @onCloseAnimationEnd
@el.classList.add 'zammad-chat--animate'
# force redraw
document.offsetHeight
# animate out
@el.style.transform = "translateY(#{remainerHeight}px)"
onCloseAnimationEnd: =>
@el.removeEventListener 'transitionend', @onCloseAnimationEnd
@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
@options.onCloseAnimationEnd?()
@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: ->
@inputDisabled = true
@input.setAttribute('contenteditable', false)
@el.querySelector('.zammad-chat-send').disabled = true
@io.close()
enableInput: ->
@inputDisabled = false
@input.setAttribute('contenteditable', true)
@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()
# Remove button, because it can no longer be used.
btn = document.querySelector(".#{ @options.buttonClass }")
if btn
btn.classList.add @options.inactiveClass
btn.style.display = 'none';
# 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')
@options.onConnectionReestablished?()
onSessionClosed: (data) ->
@addStatus @T('Chat closed by %s', data.realname)
@disableInput()
@setAgentOnlineState 'offline'
@inactiveTimeout.stop()
@options.onSessionClosed?(data)
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()
@options.onConnectionEstablished?(data)
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, '') # WebSocket may run on example.com/ws path
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()
@options.onCssLoaded?()
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
@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(//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
into for line-though
content = content.replace(/<(\/?)s>/gi, '<$1strike>')
# Replace nbsp entites to char since it's easier to handle
# content = content.replace(/ /gi, "\u00a0")
content = content.replace(/ /gi, ' ')
# Convert ___ to string of alternating
# breaking/non-breaking spaces of same length
#content = content.replace(/([\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 = ''
if (/^\s*\w+\./.test(txt))
matches = /([0-9])\./.exec(txt)
if matches
start = parseInt(matches[1], 10)
list_tag = start>1 ? '
' : '
'
else
list_tag = '
'
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', '' + p.innerHTML + ''
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