customer chat built + sources

This commit is contained in:
Felix Niklas 2015-10-15 11:14:19 +02:00
parent deded86b63
commit 67886a6af7
13 changed files with 1408 additions and 0 deletions

View file

@ -0,0 +1,227 @@
do($ = window.jQuery, window) ->
# Define the plugin class
class ZammadChat
defaults:
invitationPhrase: '<strong>Chat</strong> with us!'
agentPhrase: '%%agent%% is helping you'
show: true
target: $('body')
isOpen: false
blinkOnlineInterval: null
stopBlinOnlineStateTimeout: null
showTimeEveryXMinutes: 1
lastTimestamp: null
lastAddedType: null
inputTimeout: null
isTyping: false
isOnline: true
strings:
'Online': 'Online'
'Offline': 'Offline'
'Connecting': 'Connecting'
'Connection re-established': 'Connection re-established'
'Today': 'Today'
T: (string) ->
return @strings[string]
view: (name) ->
return window.zammadChatTemplates[name]
constructor: (el, options) ->
@options = $.extend {}, @defaults, options
@el = $(@view('chat')())
@options.target.append @el
@setAgentOnlineState @isOnline
@show() if @options.show
@el.find('.zammad-chat-header').click @toggle
@el.find('.zammad-chat-controls').on 'submit', @onSubmit
@el.find('.zammad-chat-input').on(
keydown: @checkForEnter
input: @onInput
).autoGrow { extraLine: false }
checkForEnter: (event) =>
if not event.shiftKey and event.keyCode is 13
event.preventDefault()
@sendMessage()
onInput: =>
# remove unread-state from messages
@el.find('.zammad-chat-message--unread')
.removeClass 'zammad-chat-message--unread'
clearTimeout(@inputTimeout) if @inputTimeout
# fire typingEnd after 5 seconds
@inputTimeout = setTimeout @onTypingEnd, 5000
@onTypingStart() if @isTyping
onTypingStart: ->
# send typing start event
@isTyping = true
onTypingEnd: =>
# send typing end event
@isTyping = false
onSubmit: (event) =>
event.preventDefault()
@sendMessage()
sendMessage: ->
message = @el.find('.zammad-chat-input').val()
if message
messageElement = @view('message') { message: message }
@maybeAddTimestamp()
# add message before message typing loader
if @el.find('.zammad-chat-message--typing').size()
@lastAddedType = 'typing-placeholder'
@el.find('.zammad-chat-message--typing').before messageElement
else
@lastAddedType = 'message--customer'
@el.find('.zammad-chat-body').append messageElement
@el.find('.zammad-chat-input').val('')
@scrollToBottom()
@isTyping = false
# send message event
receiveMessage: (message) =>
# hide writing indicator
@onAgentTypingEnd()
@maybeAddTimestamp()
@lastAddedType = 'message--agent'
unread = document.hidden ? " zammad-chat-message--unread" : ""
@el.find('.zammad-chat-body').append @view('message')({message: message})
@scrollToBottom()
toggle: =>
if @isOpen then @close() else @open()
open: ->
@el
.addClass('zammad-chat-is-open')
.animate { bottom: 0 }, 500, @onOpenAnimationEnd
onOpenAnimationEnd: =>
@isOpen = true
setTimeout @onConnectionEstablished, 1180
setTimeout @onAgentTypingStart, 2000
setTimeout @receiveMessage, 5000, "Hello! How can I help you?"
@connect()
close: ->
remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
@el.animate { bottom: -remainerHeight }, 500, onCloseAnimationEnd
onCloseAnimationEnd: =>
@el.removeClass('zammad-chat-is-open')
@disconnect()
@isOpen = false
hide: ->
@el.removeClass('zammad-chat-is-visible')
show: ->
@el.addClass('zammad-chat-is-visible')
remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
@el.css 'bottom', -remainerHeight
onAgentTypingStart: =>
# never display two loaders
return if @el.find('.zammad-chat-message--typing').size()
@maybeAddTimestamp()
@el.find('.zammad-chat-body').append @view('typingIndicator')()
@scrollToBottom()
onAgentTypingEnd: =>
@el.find('.zammad-chat-message--typing').remove()
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
@addStatus label, time
@lastTimestamp = timestamp
@lastAddedType = 'timestamp'
updateLastTimestamp: (label, time) ->
@el.find('.zammad-chat-body')
.find('.zammad-chat-status')
.last()
.replaceWith @view('status')
label: label
time: time
addStatus: (label, time) ->
@el.find('.zammad-chat-body').append( @view('status')
label: label
time: time
scrollToBottom: ->
@el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight'))
connect: ->
reconnect: =>
# set status to connecting
@lastAddedType = 'status'
@el.find('.zammad-chat-agent-status').attr('data-status', 'connecting').text @T('Connecting')
@addStatus @T('Connection lost')
onConnectionReestablished: =>
# set status back to online
@lastAddedType = 'status'
@el.find('.zammad-chat-agent-status').attr('data-status', 'online').text @T('Online')
@addStatus @T('Connection re-established')
disconnect: ->
@el.find('.zammad-chat-loader').removeClass('zammad-chat-is-hidden');
@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');
onConnectionEstablished: =>
@el.find('.zammad-chat-loader').addClass('zammad-chat-is-hidden');
@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');
@el.find('.zammad-chat-input').focus();
setAgentOnlineState: (state) =>
@isOnline = state
@el
.find('.zammad-chat-agent-status')
.toggleClass('zammad-chat-is-online', state)
.text if state then @T('Online') else @T('Offline')
$(document).ready ->
window.zammadChat = new ZammadChat()

292
public/assets/chat/chat.js Normal file
View file

@ -0,0 +1,292 @@
if (!window.zammadChatTemplates) {
window.zammadChatTemplates = {};
}
window.zammadChatTemplates["chat"] = function (__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
__out.push('<div class="zammad-chat">\n <div class="zammad-chat-header">\n <div class="zammad-chat-header-controls">\n <span class="zammad-chat-agent-status zammad-chat-is-hidden" data-status="online">Online</span>\n <span class="zammad-chat-toggle">\n <svg class="zammad-chat-toggle-icon-open" viewBox="0 0 13 7"><path d="M10.807 7l1.4-1.428-5-4.9L6.5-.02l-.7.7-4.9 4.9 1.414 1.413L6.5 2.886 10.807 7z" fill-rule="evenodd"/></svg>\n <svg class="zammad-chat-toggle-icon-close" viewBox="0 0 13 7"><path d="M6.554 4.214L2.246 0l-1.4 1.428 5 4.9.708.693.7-.7 4.9-4.9L10.74.008 6.553 4.214z" fill-rule="evenodd"/></svg>\n </span>\n </div>\n <div class="zammad-chat-agent zammad-chat-is-hidden">\n <img class="zammad-chat-agent-avatar" src="https://s3.amazonaws.com/uifaces/faces/twitter/joshaustin/128.jpg">\n <span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">Adam</span> is helping you.\n </span>\n </div>\n <div class="zammad-chat-welcome">\n <svg class="zammad-chat-icon" viewBox="0 0 24 24"><path d="M2 5C2 4 3 3 4 3h16c1 0 2 1 2 2v10C22 16 21 17 20 17H4C3 17 2 16 2 15V5zM12 17l6 4v-4h-6z" fill-rule="evenodd"/></svg>\n <span class="zammad-chat-welcome-text"><strong>Chat</strong> with us!</span>\n </div>\n </div>\n <div class="zammad-chat-loader">\n <span class="zammad-chat-loading-animation">\n <span class="zammad-chat-loading-circle"></span>\n <span class="zammad-chat-loading-circle"></span>\n <span class="zammad-chat-loading-circle"></span>\n </span>\n <span class="zammad-chat-loader-text">Connecting</span>\n </div>\n <div class="zammad-chat-body"></div>\n <form class="zammad-chat-controls">\n <textarea class="zammad-chat-input" rows="1" placeholder="Compose your message..."></textarea>\n <button type="submit" class="zammad-chat-send">Send</button>\n </form>\n</div>');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
};
/*!
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* <jevin9@gmail.com> wrote this file. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return. Jevin O. Sewaruth
* ----------------------------------------------------------------------------
*
* Autogrow Textarea Plugin Version v3.0
* http://www.technoreply.com/autogrow-textarea-plugin-3-0
*
* THIS PLUGIN IS DELIVERD ON A PAY WHAT YOU WHANT BASIS. IF THE PLUGIN WAS USEFUL TO YOU, PLEASE CONSIDER BUYING THE PLUGIN HERE :
* https://sites.fastspring.com/technoreply/instant/autogrowtextareaplugin
*
* Date: October 15, 2012
*/
jQuery.fn.autoGrow = function(options) {
return this.each(function() {
var settings = jQuery.extend({
extraLine: true,
}, options);
var createMirror = function(textarea) {
jQuery(textarea).after('<div class="autogrow-textarea-mirror"></div>');
return jQuery(textarea).next('.autogrow-textarea-mirror')[0];
}
var sendContentToMirror = function (textarea) {
mirror.innerHTML = String(textarea.value)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/ /g, '&nbsp;')
.replace(/\n/g, '<br />') +
(settings.extraLine? '.<br/>.' : '')
;
if (jQuery(textarea).height() != jQuery(mirror).height())
jQuery(textarea).height(jQuery(mirror).height());
}
var growTextarea = function () {
sendContentToMirror(this);
}
// Create a mirror
var mirror = createMirror(this);
// Style the mirror
mirror.style.display = 'none';
mirror.style.wordWrap = 'break-word';
mirror.style.whiteSpace = 'normal';
mirror.style.padding = jQuery(this).css('paddingTop') + ' ' +
jQuery(this).css('paddingRight') + ' ' +
jQuery(this).css('paddingBottom') + ' ' +
jQuery(this).css('paddingLeft');
mirror.style.width = jQuery(this).css('width');
mirror.style.fontFamily = jQuery(this).css('font-family');
mirror.style.fontSize = jQuery(this).css('font-size');
mirror.style.lineHeight = jQuery(this).css('line-height');
// Style the textarea
this.style.overflow = "hidden";
this.style.minHeight = this.rows+"em";
// Bind the textarea's event
this.onkeyup = growTextarea;
// Fire the event for text already present
sendContentToMirror(this);
});
};
if (!window.zammadChatTemplates) {
window.zammadChatTemplates = {};
}
window.zammadChatTemplates["message"] = function (__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
__out.push('<div class="zammad-chat-message zammad-chat-message--customer">\n <span class="zammad-chat-message-body">');
__out.push(message);
__out.push('</span>\n</div>');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
};
if (!window.zammadChatTemplates) {
window.zammadChatTemplates = {};
}
window.zammadChatTemplates["status"] = function (__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
__out.push('<div class="zammad-chat-status"><strong>');
__out.push(__sanitize(label));
__out.push('</strong>');
__out.push(__sanitize(time));
__out.push('</div>');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
};
if (!window.zammadChatTemplates) {
window.zammadChatTemplates = {};
}
window.zammadChatTemplates["typingIndicator"] = function (__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
__out.push('<div class="zammad-chat-message zammad-chat-message--typing zammad-chat-message--agent">\n <span class="zammad-chat-message-body">\n <span class="zammad-chat-loading-animation">\n <span class="zammad-chat-loading-circle"></span>\n <span class="zammad-chat-loading-circle"></span>\n <span class="zammad-chat-loading-circle"></span>\n </span>\n </span>\n</div>');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
};

1
public/assets/chat/chat.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,52 @@
var gulp = require('gulp');
var autoprefixer = require('gulp-autoprefixer');
var sass = require('gulp-sass');
var gutil = require('gulp-util');
var concat = require('gulp-concat');
var coffee = require('gulp-coffee');
var eco = require('gulp-eco');
var rename = require('gulp-rename');
var uglify = require('gulp-uglify');
var merge = require('merge-stream');
var plumber = require('gulp-plumber');
gulp.task('css', function(){
return gulp.src('style.scss')
.pipe(sass.sync().on('error', gutil.log))
.pipe(autoprefixer({
browsers: ['last 4 versions'],
cascade: false
}))
.pipe(gulp.dest('./'));
});
gulp.task('js', function(){
var templates = gulp.src('views/*.eco')
.pipe(eco({namespace: 'zammadChatTemplates'}));
var js = gulp.src('chat.coffee')
.pipe(plumber())
.pipe(coffee({bare: true}).on('error', gutil.log));
var autoGrow = gulp.src('jquery.autoGrow.js');
return merge(templates, js, autoGrow)
.pipe(concat('chat.js'))
.pipe(gulp.dest('./'))
.pipe(uglify())
.pipe(rename({ extname: '.min.js' }))
.pipe(gulp.dest('./'));
});
gulp.task('default', function(){
var cssWatcher = gulp.watch('style.scss', ['css']);
cssWatcher.on('change', function(event) {
console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
});
var jsWatcher = gulp.watch(['chat.coffee', 'views/*.eco'], ['js']);
jsWatcher.on('change', function(event) {
console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
});
});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,77 @@
/*!
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* <jevin9@gmail.com> wrote this file. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return. Jevin O. Sewaruth
* ----------------------------------------------------------------------------
*
* Autogrow Textarea Plugin Version v3.0
* http://www.technoreply.com/autogrow-textarea-plugin-3-0
*
* THIS PLUGIN IS DELIVERD ON A PAY WHAT YOU WHANT BASIS. IF THE PLUGIN WAS USEFUL TO YOU, PLEASE CONSIDER BUYING THE PLUGIN HERE :
* https://sites.fastspring.com/technoreply/instant/autogrowtextareaplugin
*
* Date: October 15, 2012
*/
jQuery.fn.autoGrow = function(options) {
return this.each(function() {
var settings = jQuery.extend({
extraLine: true,
}, options);
var createMirror = function(textarea) {
jQuery(textarea).after('<div class="autogrow-textarea-mirror"></div>');
return jQuery(textarea).next('.autogrow-textarea-mirror')[0];
}
var sendContentToMirror = function (textarea) {
mirror.innerHTML = String(textarea.value)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/ /g, '&nbsp;')
.replace(/\n/g, '<br />') +
(settings.extraLine? '.<br/>.' : '')
;
if (jQuery(textarea).height() != jQuery(mirror).height())
jQuery(textarea).height(jQuery(mirror).height());
}
var growTextarea = function () {
sendContentToMirror(this);
}
// Create a mirror
var mirror = createMirror(this);
// Style the mirror
mirror.style.display = 'none';
mirror.style.wordWrap = 'break-word';
mirror.style.whiteSpace = 'normal';
mirror.style.padding = jQuery(this).css('paddingTop') + ' ' +
jQuery(this).css('paddingRight') + ' ' +
jQuery(this).css('paddingBottom') + ' ' +
jQuery(this).css('paddingLeft');
mirror.style.width = jQuery(this).css('width');
mirror.style.fontFamily = jQuery(this).css('font-family');
mirror.style.fontSize = jQuery(this).css('font-size');
mirror.style.lineHeight = jQuery(this).css('line-height');
// Style the textarea
this.style.overflow = "hidden";
this.style.minHeight = this.rows+"em";
// Bind the textarea's event
this.onkeyup = growTextarea;
// Fire the event for text already present
sendContentToMirror(this);
});
};

View file

@ -0,0 +1,19 @@
{
"name": "zammad-chat",
"version": "1.0.0",
"description": "Zammad Customer Chat Javascript Widget",
"devDependencies": {
"gulp": "^3.9.0",
"gulp-autoprefixer": "^3.0.2",
"gulp-coffee": "^2.3.1",
"gulp-concat": "^2.6.0",
"gulp-eco": "0.0.2",
"gulp-plumber": "^1.0.1",
"gulp-rename": "^1.2.2",
"gulp-sass": "^2.0.4",
"gulp-uglify": "^1.4.2",
"gulp-util": "^3.0.6",
"merge-stream": "^1.0.0",
"sass.js": "^0.9.2"
}
}

View file

@ -0,0 +1,329 @@
.zammad-chat {
color: black;
position: fixed;
right: 30px;
bottom: 0;
font-size: 12px;
width: 33em;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
border-radius: 5px 5px 0 0;
will-change: bottom;
display: none;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column; }
.zammad-chat.zammad-chat-is-open {
display: -webkit-flex;
display: -ms-flexbox;
display: flex; }
.zammad-chat-icon {
height: 2em;
fill: currentColor;
vertical-align: top;
margin-right: 5px;
margin-top: 4px; }
.zammad-chat-header {
padding: 0.5em 2.5em 0.5em 1em;
background: #379ad7;
color: white;
line-height: 2.5em;
box-shadow: 0 -1px #247fb7, 0 1px rgba(255, 255, 255, 0.3) inset, 0 -1px #247fb7 inset, 0 1px 1px rgba(0, 0, 0, 0.13);
position: relative;
border-radius: 5px 5px 0 0;
overflow: hidden;
cursor: pointer; }
.zammad-chat-welcome-text {
font-size: 1.2em; }
.zammad-chat-toggle {
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 3.4em;
text-align: center;
line-height: 3.5em; }
.zammad-chat-toggle-icon-open,
.zammad-chat-toggle-icon-close {
fill: currentColor;
height: 0.85em; }
.zammad-chat-toggle-icon-close,
.zammad-chat.zammad-chat-is-open .zammad-chat-toggle-icon-open {
display: none; }
.zammad-chat.zammad-chat-is-open .zammad-chat-toggle-icon-close {
display: inline; }
.zammad-chat-agent {
float: left; }
.zammad-chat-header-controls {
float: right; }
.zammad-chat-agent-avatar {
border-radius: 100%;
margin-right: 0.6em;
float: left;
width: 2.5em; }
.zammad-chat-agent-name {
font-weight: bold; }
.zammad-chat-agent-status {
margin: 0 1em;
display: inline-block;
line-height: 2em;
padding: 0 0.7em;
border-radius: 1em;
background: #288ecc;
box-shadow: 0 0 0 1px #2582bb; }
.zammad-chat-agent-status:before {
content: "";
background: #f35912;
display: inline-block;
height: 0.9em;
width: 0.9em;
border-radius: 100%;
position: relative;
margin-right: 0.3em;
box-shadow: 0 0 0 1px #2582bb; }
.zammad-chat-agent-status[data-status="online"]:before {
background: #52c782; }
.zammad-chat-agent-status[data-status="connecting"]:before {
-webkit-animation: linear connect-fade 600ms infinite alternate;
animation: linear connect-fade 600ms infinite alternate;
background: #faab00; }
@-webkit-keyframes connect-fade {
from {
opacity: .5;
-webkit-transform: scale(0.6);
transform: scale(0.6); }
to {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1); } }
@keyframes connect-fade {
from {
opacity: .5;
-webkit-transform: scale(0.6);
transform: scale(0.6); }
to {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1); } }
.zammad-chat-loader {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 3.5em;
margin-top: 1px;
text-align: center;
background: radial-gradient(rgba(255, 255, 255, 0.75), white);
z-index: 1;
padding: 10px;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center; }
.zammad-chat-loader-text {
font-size: 1.3em; }
.zammad-chat-loader .zammad-chat-loading-animation {
margin-right: 8px;
margin-left: -19px; }
.zammad-chat-body {
padding: 0.5em 1em;
height: 300px;
overflow: auto;
background: white; }
.zammad-chat-status {
text-align: center;
color: #999999;
font-size: 0.8em;
margin: 1em 0; }
.zammad-chat-message {
margin: 0.5em 0; }
.zammad-chat-message-body {
padding: 0.6em 1em;
border-radius: 1em;
background: #f6f8f9;
display: inline-block;
max-width: 70%;
white-space: pre;
box-shadow: 0 2px rgba(255, 255, 255, 0.15) inset, 0 0 0 1px rgba(0, 0, 0, 0.08) inset, 0 1px rgba(0, 0, 0, 0.02); }
.zammad-chat-message--customer {
text-align: right; }
.zammad-chat-message--customer + .zammad-chat-message--agent,
.zammad-chat-message--agent + .zammad-chat-message--customer {
margin-top: 1em; }
.zammad-chat-message--customer .zammad-chat-message-body {
background: #379ad7;
color: white; }
.zammad-chat-message--unread {
font-weight: bold; }
.zammad-chat-message--typing .zammad-chat-message-body {
white-space: normal; }
.zammad-chat-loading-circle {
background: #c5dded;
border-radius: 100%;
height: 0.72em;
width: 0.72em;
display: inline-block;
-webkit-animation: ease-in-out load-fade 600ms infinite alternate;
animation: ease-in-out load-fade 600ms infinite alternate; }
.zammad-chat-loading-circle + .zammad-chat-loading-circle {
-webkit-animation-delay: .13s;
animation-delay: .13s; }
.zammad-chat-loading-circle + .zammad-chat-loading-circle + .zammad-chat-loading-circle {
-webkit-animation-delay: .26s;
animation-delay: .26s; }
@-webkit-keyframes load-fade {
from {
opacity: .5;
-webkit-transform: scale(0.6);
transform: scale(0.6); }
67% {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1); } }
@keyframes load-fade {
from {
opacity: .5;
-webkit-transform: scale(0.6);
transform: scale(0.6); }
67% {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1); } }
.zammad-chat-controls {
overflow: hidden;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: flex-start;
-ms-flex-align: start;
align-items: flex-start;
border-top: 1px solid #e3f0f7;
padding: 0;
line-height: 1.4em;
box-shadow: 0 1px rgba(0, 0, 0, 0.01), 0 -1px rgba(0, 0, 0, 0.02);
position: relative;
background: white; }
.zammad-chat-input {
float: left;
font-family: inherit;
line-height: inherit;
font-size: inherit;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
background: none;
box-shadow: none;
padding: 1em 2em;
outline: none;
resize: none;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
max-height: 6em; }
.zammad-chat-input::-webkit-input-placeholder {
color: #bdc9d0; }
.zammad-chat-send {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
float: right;
font-family: inherit;
font-size: inherit;
background: #379ad7;
color: white;
padding: 0.6em 1.2em;
margin: 0.5em 1em 0.5em;
cursor: pointer;
border: none;
border-radius: 1.5em;
box-shadow: 0 2px rgba(255, 255, 255, 0.25) inset, 0 0 0 1px #247fb7 inset, 0 1px rgba(0, 0, 0, 0.1);
outline: none; }
.zammad-chat-is-hidden {
display: none; }
.zammad-chat-is-visible {
display: block; }
/*
# Flat Design
*/
.zammad-chat--flat .zammad-chat-header,
.zammad-chat--flat .zammad-chat-body {
border: none; }
.zammad-chat--flat .zammad-chat-header {
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.13); }
.zammad-chat--flat .zammad-chat-message-body {
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08) inset; }
.zammad-chat--flat .zammad-chat-agent-status,
.zammad-chat--flat .zammad-chat-send {
box-shadow: none; }
/*
Mobile Design
*/
@media only screen and (max-width: 768px) {
.zammad-chat {
left: 0;
right: 0;
width: auto;
height: 100vh; }
.zammad-chat-body {
height: auto;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1; }
.zammad-chat,
.zammad-chat-header {
border-radius: 0 !important; } }

View file

@ -0,0 +1,360 @@
/// Returns the luminance of `$color` as a float (between 0 and 1)
/// 1 is pure white, 0 is pure black
/// @param {Color} $color - Color
/// @return {Number}
/// @link http://stackoverflow.com/a/596241/731172
@function luminance($color) {
@return (0.375 * red($color) + 0.5 * green($color) + 0.125 * blue($color)) / 255;
}
/// Limits the lighten function to a maximum value
/// @param {Color} $color - Color
/// @param {Number} $amount - The amount to increase the lightness by, between 0% and 100%
/// @param {Number} $max - The maximal lightness amount
/// @return {Color}
@function lightenMax($color, $amount, $max) {
$enlightedColor: lighten($color, $amount);
@if lightness($enlightedColor) > $max {
@return change-color($color, $lightness: $max);
} @else {
@return $enlightedColor;
}
}
$themeColor: hsl(203,67,53);
$fontSize: 12px;
$borderRadius: 5px;
$luminance: luminance($themeColor);
$contrastTextColor: if($luminance > 0.5, inherit, white);
$baseTextColor: if($luminance < 0.2, white, black);
.zammad-chat {
color: $baseTextColor;
position: fixed;
right: 30px;
bottom: 0;
font-size: $fontSize;
width: 33em;
box-shadow: 0 3px 10px rgba(0,0,0,.3);
border-radius: $borderRadius $borderRadius 0 0;
will-change: bottom;
display: none;
flex-direction: column;
}
.zammad-chat.zammad-chat-is-open {
display: flex;
}
.zammad-chat-icon {
height: 2em;
fill: currentColor;
vertical-align: top;
margin-right: 5px;
margin-top: 4px;
}
.zammad-chat-header {
padding: 0.5em 2.5em 0.5em 1em;
background: $themeColor;
color: $contrastTextColor;
line-height: 2.5em;
box-shadow:
0 -1px darken($themeColor, 10%),
0 1px rgba(255,255,255,.3) inset,
0 -1px darken($themeColor, 10%) inset,
0 1px 1px rgba(0,0,0,.13);
position: relative;
border-radius: $borderRadius $borderRadius 0 0;
overflow: hidden;
cursor: pointer;
}
.zammad-chat-welcome-text {
font-size: 1.2em;
}
.zammad-chat-toggle {
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 3.4em;
text-align: center;
line-height: 3.5em;
}
.zammad-chat-toggle-icon-open,
.zammad-chat-toggle-icon-close {
fill: currentColor;
height: 0.85em;
}
.zammad-chat-toggle-icon-close,
.zammad-chat.zammad-chat-is-open .zammad-chat-toggle-icon-open {
display: none;
}
.zammad-chat.zammad-chat-is-open .zammad-chat-toggle-icon-close {
display: inline;
}
.zammad-chat-agent {
float: left;
}
.zammad-chat-header-controls {
float: right;
}
.zammad-chat-agent-avatar {
border-radius: 100%;
margin-right: 0.6em;
float: left;
width: 2.5em;
}
.zammad-chat-agent-name {
font-weight: bold;
}
.zammad-chat-agent-status {
margin: 0 1em;
display: inline-block;
line-height: 2em;
padding: 0 .7em;
border-radius: 1em;
background: darken($themeColor, 5%);
box-shadow: 0 0 0 1px darken($themeColor, 9%);
}
.zammad-chat-agent-status:before {
content: "";
background: hsl(19,90%,51%);
display: inline-block;
height: 0.9em;
width: 0.9em;
border-radius: 100%;
position: relative;
margin-right: 0.3em;
box-shadow: 0 0 0 1px darken($themeColor, 9%);
}
.zammad-chat-agent-status[data-status="online"]:before {
background: hsl(145,51%,55%);
}
.zammad-chat-agent-status[data-status="connecting"]:before {
animation: linear connect-fade 600ms infinite alternate;
background: hsl(41,100%,49%);
}
@keyframes connect-fade {
from { opacity: .5; transform: scale(0.6); }
to { opacity: 1; transform: scale(1); }
}
.zammad-chat-loader {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 3.5em;
margin-top: 1px;
text-align: center;
background: radial-gradient(rgba(255,255,255,.75), rgba(255,255,255,1));
z-index: 1;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.zammad-chat-loader-text {
font-size: 1.3em;
}
.zammad-chat-loader .zammad-chat-loading-animation {
margin-right: 8px;
margin-left: -19px;
}
.zammad-chat-body {
padding: 0.5em 1em;
height: 300px;
overflow: auto;
background: white;
}
.zammad-chat-status {
text-align: center;
color: hsl(0,0%,60%);
font-size: 0.8em;
margin: 1em 0;
}
.zammad-chat-message {
margin: 0.5em 0;
}
.zammad-chat-message-body {
padding: 0.6em 1em;
border-radius: 1em;
background: desaturate(lightenMax($themeColor, 50%, 97%), 50%);
display: inline-block;
max-width: 70%;
white-space: pre;
box-shadow:
0 2px rgba(255,255,255,.15) inset,
0 0 0 1px rgba(0,0,0,.08) inset,
0 1px rgba(0,0,0,.02);
}
.zammad-chat-message--customer {
text-align: right;
}
.zammad-chat-message--customer + .zammad-chat-message--agent,
.zammad-chat-message--agent + .zammad-chat-message--customer {
margin-top: 1em;
}
.zammad-chat-message--customer .zammad-chat-message-body {
background: $themeColor;
color: $contrastTextColor;
}
.zammad-chat-message--unread {
font-weight: bold;
}
.zammad-chat-message--typing .zammad-chat-message-body {
white-space: normal;
}
.zammad-chat-loading-circle {
background: desaturate(lightenMax($themeColor, 50%, 85%), 15%);
border-radius: 100%;
height: 0.72em;
width: 0.72em;
display: inline-block;
animation: ease-in-out load-fade 600ms infinite alternate;
}
.zammad-chat-loading-circle + .zammad-chat-loading-circle {
animation-delay: .13s;
}
.zammad-chat-loading-circle + .zammad-chat-loading-circle + .zammad-chat-loading-circle {
animation-delay: .26s;
}
@keyframes load-fade {
from { opacity: .5; transform: scale(0.6); }
67% { opacity: 1; transform: scale(1); }
}
.zammad-chat-controls {
overflow: hidden;
display: flex;
align-items: flex-start;
border-top: 1px solid desaturate(lightenMax($themeColor, 80%, 93%), 10%);
padding: 0;
line-height: 1.4em;
box-shadow:
0 1px rgba(0,0,0,.01),
0 -1px rgba(0,0,0,.02);
position: relative;
background: white;
}
.zammad-chat-input {
float: left;
font-family: inherit;
line-height: inherit;
font-size: inherit;
appearance: none;
border: none;
background: none;
box-shadow: none;
padding: 1em 2em;
outline: none;
resize: none;
flex: 1;
max-height: 6em;
}
.zammad-chat-input::-webkit-input-placeholder {
color: desaturate(lightenMax($themeColor, 25%, 85%), 50%);
}
.zammad-chat-send {
appearance: none;
float: right;
font-family: inherit;
font-size: inherit;
background: $themeColor;
color: $contrastTextColor;
padding: 0.6em 1.2em;
margin: 0.5em 1em 0.5em;
cursor: pointer;
border: none;
border-radius: 1.5em;
box-shadow:
0 2px rgba(255,255,255,.25) inset,
0 0 0 1px darken($themeColor, 10%) inset,
0 1px rgba(0,0,0,.1);
outline: none;
}
.zammad-chat-is-hidden {
display: none;
}
.zammad-chat-is-visible {
display: block;
}
/*
# Flat Design
*/
.zammad-chat--flat .zammad-chat-header,
.zammad-chat--flat .zammad-chat-body {
border: none;
}
.zammad-chat--flat .zammad-chat-header {
box-shadow: 0 1px 1px rgba(0,0,0,.13);
}
.zammad-chat--flat .zammad-chat-message-body {
box-shadow: 0 0 0 1px rgba(0,0,0,.08) inset;
}
.zammad-chat--flat .zammad-chat-agent-status,
.zammad-chat--flat .zammad-chat-send {
box-shadow: none;
}
/*
Mobile Design
*/
@media only screen and (max-width: 768px) {
.zammad-chat {
left: 0;
right: 0;
width: auto;
height: 100vh;
}
.zammad-chat-body {
height: auto;
flex: 1;
}
.zammad-chat,
.zammad-chat-header {
border-radius: 0 !important;
}
}

View file

@ -0,0 +1,34 @@
<div class="zammad-chat">
<div class="zammad-chat-header">
<div class="zammad-chat-header-controls">
<span class="zammad-chat-agent-status zammad-chat-is-hidden" data-status="online">Online</span>
<span class="zammad-chat-toggle">
<svg class="zammad-chat-toggle-icon-open" viewBox="0 0 13 7"><path d="M10.807 7l1.4-1.428-5-4.9L6.5-.02l-.7.7-4.9 4.9 1.414 1.413L6.5 2.886 10.807 7z" fill-rule="evenodd"/></svg>
<svg class="zammad-chat-toggle-icon-close" viewBox="0 0 13 7"><path d="M6.554 4.214L2.246 0l-1.4 1.428 5 4.9.708.693.7-.7 4.9-4.9L10.74.008 6.553 4.214z" fill-rule="evenodd"/></svg>
</span>
</div>
<div class="zammad-chat-agent zammad-chat-is-hidden">
<img class="zammad-chat-agent-avatar" src="https://s3.amazonaws.com/uifaces/faces/twitter/joshaustin/128.jpg">
<span class="zammad-chat-agent-sentence">
<span class="zammad-chat-agent-name">Adam</span> is helping you.
</span>
</div>
<div class="zammad-chat-welcome">
<svg class="zammad-chat-icon" viewBox="0 0 24 24"><path d="M2 5C2 4 3 3 4 3h16c1 0 2 1 2 2v10C22 16 21 17 20 17H4C3 17 2 16 2 15V5zM12 17l6 4v-4h-6z" fill-rule="evenodd"/></svg>
<span class="zammad-chat-welcome-text"><strong>Chat</strong> with us!</span>
</div>
</div>
<div class="zammad-chat-loader">
<span class="zammad-chat-loading-animation">
<span class="zammad-chat-loading-circle"></span>
<span class="zammad-chat-loading-circle"></span>
<span class="zammad-chat-loading-circle"></span>
</span>
<span class="zammad-chat-loader-text">Connecting</span>
</div>
<div class="zammad-chat-body"></div>
<form class="zammad-chat-controls">
<textarea class="zammad-chat-input" rows="1" placeholder="Compose your message..."></textarea>
<button type="submit" class="zammad-chat-send">Send</button>
</form>
</div>

View file

@ -0,0 +1,3 @@
<div class="zammad-chat-message zammad-chat-message--customer">
<span class="zammad-chat-message-body"><%- message %></span>
</div>

View file

@ -0,0 +1 @@
<div class="zammad-chat-status"><strong><%= label %></strong><%= time %></div>

View file

@ -0,0 +1,9 @@
<div class="zammad-chat-message zammad-chat-message--typing zammad-chat-message--agent">
<span class="zammad-chat-message-body">
<span class="zammad-chat-loading-animation">
<span class="zammad-chat-loading-circle"></span>
<span class="zammad-chat-loading-circle"></span>
<span class="zammad-chat-loading-circle"></span>
</span>
</span>
</div>