do($ = window.jQuery, 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 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>/g, '
')
console.log('p', docType, text)
if docType is 'html'
sanitized = DOMPurify.sanitize(text)
@log.debug 'sanitized HTML clipboard', sanitized
html = $("
#{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
html = $(html)
html.contents().each( ->
if @nodeType == 8
$(@).remove()
)
# remove tags, keep content
html.find('a, font, small, time, form, label').replaceWith( ->
$(@).contents()
)
# replace tags with generic div
# New type of the tag
replacementTag = 'div';
# Replace all x tags with the type of replacementTag
html.find('textarea').each( ->
outer = @outerHTML
# Replace opening tag
regex = new RegExp('<' + @tagName, 'i')
newTag = outer.replace(regex, '<' + replacementTag)
# Replace closing tag
regex = new RegExp('' + @tagName, 'i')
newTag = newTag.replace(regex, '' + replacementTag)
$(@).replaceWith(newTag)
)
# remove tags & content
html.find('font, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset').remove()
@removeAttributes(html)
text = html.html()
# as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower)
if docType is 'text3'
@pasteHtmlAtCaret(text)
else
document.execCommand('insertHTML', false, text)
true
)
@input.on('drop', (e) =>
e.stopPropagation()
e.preventDefault()
dataTransfer
if window.dataTransfer # ie
dataTransfer = window.dataTransfer
else if e.originalEvent.dataTransfer # other browsers
dataTransfer = e.originalEvent.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) =>
result = e.target.result
img = document.createElement('img')
img.src = result
# Insert the image at the carat
insert = (dataUrl, width, height, isRetina) =>
# adapt image if we are on retina devices
if @isRetina()
width = width / 2
height = height / 2
result = dataUrl
img = $("
")
img = img.get(0)
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(img.src, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
reader.readAsDataURL(file)
)
$(window).on('beforeunload', =>
@onLeaveTemporary()
)
$(window).bind('hashchange', =>
if @isOpen
if @sessionId
@send 'chat_session_notice',
session_id: @sessionId
message: window.location.href
return
@idleTimeout.start()
)
if @isFullscreen
@input.on
focus: @onFocus
focusout: @onFocusOut
stopPropagation: (event) ->
event.stopPropagation()
checkForEnter: (event) =>
if not @inputDisabled and 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_session_notice'
@addStatus @T(pipe.data.message)
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'
$(".#{ @options.buttonClass }").click(@open).removeClass(@options.inactiveClass)
@options.onReady?()
if @options.show
@show()
onError: (message) =>
@log.debug message
@addStatus(message)
$(".#{ @options.buttonClass }").hide()
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.html(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.html()
@onTyping()
onFocus: =>
$(window).scrollTop(10)
keyboardShown = $(window).scrollTop() > 0
$(window).scrollTop(0)
if keyboardShown
@log.notice 'virtual keyboard shown'
# on keyboard shown
# can't measure visible area height :(
onFocusOut: ->
# on keyboard hidden
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.html()
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').get(0)
@lastAddedType = 'typing-placeholder'
@el.find('.zammad-chat-message--typing').before messageElement
else
@lastAddedType = 'message--customer'
@el.find('.zammad-chat-body').append messageElement
@input.html('')
@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 ''
@el.find('.zammad-chat-body').append @view('message')(data)
open: =>
if @isOpen
@log.debug 'widget already open, block'
return
@isOpen = true
@log.debug 'open widget'
@show()
if !@sessionId
@showLoader()
@el.addClass('zammad-chat-is-open')
remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
@el.css 'bottom', -remainerHeight
if !@sessionId
@el.animate { bottom: 0 }, 500, @onOpenAnimationEnd
@send('chat_session_init'
url: window.location.href
)
else
@el.css 'bottom', 0
@onOpenAnimationEnd()
onOpenAnimationEnd: =>
@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.height() - @el.find('.zammad-chat-header').outerHeight()
@el.animate { bottom: -remainerHeight }, 500, @onCloseAnimationEnd
onCloseAnimationEnd: =>
@el.css 'bottom', ''
@el.removeClass('zammad-chat-is-open')
@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
@options.onCloseAnimationEnd?()
@io.reconnect()
onWebSocketClose: =>
return if @isOpen
if @el
@el.removeClass('zammad-chat-is-shown')
@el.removeClass('zammad-chat-is-loaded')
show: ->
return if @state is 'offline'
@el.addClass('zammad-chat-is-loaded')
@el.addClass('zammad-chat-is-shown')
disableInput: ->
@inputDisabled = true
@input.prop('contenteditable', false)
@el.find('.zammad-chat-send').prop('disabled', true)
@io.close()
enableInput: ->
@inputDisabled = false
@input.prop('contenteditable', true)
@el.find('.zammad-chat-send').prop('disabled', false)
hideModal: ->
@el.find('.zammad-chat-modal').html ''
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-modal').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').get(0)
@maybeAddTimestamp()
@el.find('.zammad-chat-body').append @view('typingIndicator')()
# only if typing indicator is shown
return if !@isVisible(@el.find('.zammad-chat-message--typing'), true)
@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()
detectScrolledtoBottom: =>
scrollBottom = @el.find('.zammad-chat-body').scrollTop() + @el.find('.zammad-chat-body').outerHeight()
@scrolledToBottom = Math.abs(scrollBottom - @el.find('.zammad-chat-body').prop('scrollHeight')) <= @scrollSnapTolerance
@el.find('.zammad-scroll-hint').addClass('is-hidden') if @scrolledToBottom
showScrollHint: ->
@el.find('.zammad-scroll-hint').removeClass('is-hidden')
# compensate scroll
@el.find('.zammad-chat-body').scrollTop(@el.find('.zammad-chat-body').scrollTop() + @el.find('.zammad-scroll-hint').outerHeight())
onScrollHintClick: =>
# animate scroll
@el.find('.zammad-chat-body').animate({scrollTop: @el.find('.zammad-chat-body').prop('scrollHeight')}, 300)
scrollToBottom: ({ showHint } = { showHint: false }) ->
if @scrolledToBottom
@el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('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.
$(".#{ @options.buttonClass }").hide()
# 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
@el.find('.zammad-chat-body').html('')
@el.find('.zammad-chat-agent').html @view('agent')
agent: @agent
@enableInput()
@hideModal()
@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() if not @isFullscreen
@setAgentOnlineState 'online'
@waitingListTimeout.stop()
@idleTimeout.stop()
@inactiveTimeout.start()
@options.onConnectionEstablished?(data)
showCustomerTimeout: ->
@el.find('.zammad-chat-modal').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-modal').html @view('waiting_list_timeout')
delay: @options.watingListTimeout
reload = ->
location.reload()
@el.find('.js-restart').click reload
@sessionClose()
showLoader: ->
@el.find('.zammad-chat-modal').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 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.css
overflow: 'hidden'
position: 'fixed'
enableScrollOnRoot: ->
@scrollRoot.scrollTop @rootScrollOffset
@scrollRoot.css
overflow: ''
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
$w = $(window)
$t = if el.length > 1 then el.eq(0) else el
t = $t.get(0)
vpWidth = $w.width()
vpHeight = $w.height()
direction = if direction then direction else 'both'
clientSize = if hidden is true then t.offsetWidth * t.offsetHeight else true
if typeof t.getBoundingClientRect is 'function'
# Use this native browser method, if available.
rec = t.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
else
viewTop = $w.scrollTop()
viewBottom = viewTop + vpHeight
viewLeft = $w.scrollLeft()
viewRight = viewLeft + vpWidth
offset = $t.offset()
_top = offset.top
_bottom = _top + $t.height()
_left = offset.left
_right = _left + $t.width()
compareTop = if partial is true then _bottom else _top
compareBottom = if partial is true then _top else _bottom
compareLeft = if partial is true then _right else _left
compareRight = if partial is true then _left else _right
if direction is 'both'
return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop)) && ((compareRight <= viewRight) && (compareLeft >= viewLeft))
else if direction is 'vertical'
return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop))
else if direction is 'horizontal'
return !!clientSize && ((compareRight <= viewRight) && (compareLeft >= viewLeft))
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.html(content)
# Parse out list indent level for lists
$('p', editor).each( ->
str = $(@).attr('style')
matches = /mso-list:\w+ \w+([0-9]+)/.exec(str)
if matches
$(@).data('_listLevel', parseInt(matches[1], 10))
)
# Parse Lists
last_level = 0
pnt = null
$('p', editor).each(->
cur_level = $(@).data('_listLevel')
if cur_level != undefined
txt = $(@).text()
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
$(@).before(list_tag)
pnt = $(@).prev()
else
pnt = $(list_tag).appendTo(pnt)
if cur_level < last_level
for i in [i..last_level-cur_level]
pnt = pnt.parent()
$('span:first', @).remove()
pnt.append('' + $(@).html() + '')
$(@).remove()
last_level = cur_level
else
last_level = 0
)
$('[style]', editor).removeAttr('style')
$('[align]', editor).removeAttr('align')
$('span', editor).replaceWith(->
$(@).contents()
)
$('span:empty', editor).remove()
$("[class^='Mso']", editor).removeAttr('class')
$('p:empty', editor).remove()
editor
removeAttribute: (element) ->
return if !element
$element = $(element)
for att in element.attributes
if att && att.name
element.removeAttribute(att.name)
#$element.removeAttr(att.name)
$element.removeAttr('style')
.removeAttr('class')
.removeAttr('lang')
.removeAttr('type')
.removeAttr('align')
.removeAttr('id')
.removeAttr('wrap')
.removeAttr('title')
removeAttributes: (html, parent = true) =>
if parent
html.each((index, element) => @removeAttribute(element) )
html.find('*').each((index, element) => @removeAttribute(element) )
html
window.ZammadChat = ZammadChat