File: //home/arjun/projects/buyercall_new/buyercall/buyercall/assets/components/widgets/call_widget.js
/* jshint esversion: 6 */
var _ = require('underscore');
var __ = require('localize');
var Backbone = require('backbone');
var utils = require('../utils');
var moment = require('moment');
require('validate/jquery.validate');
require('validate/additional-methods');
require('moment-timezone');
var animate = require('./call_widget_animation.js');
var moment = require('moment');
require('moment-timezone');
require('./styles/call_widget.scss');
var beforeCallTemplate = require('./templates/call_widget_beforecall.tpl');
var unavailableTemplate = require('./templates/call_widget_unavailable.tpl');
var inProgressTemplate = require('./templates/call_widget_inprogress.tpl');
var errorTemplate = require('./templates/call_widget_error.tpl');
var thankYouTemplate = require('./templates/call_widget_thankyou.tpl');
var closedTemplate = require('./templates/call_widget_closed.tpl');
const defaultSettings = require('./default_settings');
const defaultImages = require('./default_images');
const WidgetModel = require('./models');
// The URLs to retrieve settings and post lead information. Since the widget
// is loaded on another site, the SERVER_NAME environment variable must be
// initialized with the URL of the site (default: buyercall.com)
const URL_ROOT = `${URL_SCHEME}://${SERVER_NAME}/api`;
const CALL_URL = `${URL_ROOT}/call`;
const SAVE_URL = `${URL_ROOT}/save_lead`;
const STATUS_URL = `${URL_ROOT}/call_status`;
const SETTINGS_URL = `${URL_ROOT}/outbound/settings/`;
const ANIMATION_INTERVAL = 20;
const CALL_POLL_TIMEOUT = 360;
const ERR_NO_AGENTS_MSG = 'Sadly, no agents are available at the moment. Please try again later.';
const ERR_TOOLONG_MSG = 'The call seems to be taking longer than usual. An agent might not be available right now. We will call you back as soon as possible.';
const ERR_GENERIC_MSG = 'We\'re sorry, your request cannot be fulfilled at the moment. Please try again later.';
var LocalStorage = function (guid) {
function getAll() {
try {
var allStorage = JSON.parse(window.localStorage['buyercall']);
if (guid in allStorage) {
return allStorage[guid];
}
} catch (e) { }
return {};
}
function setAll(dict) {
if (!window.localStorage)
return;
var allStorage = JSON.parse(window.localStorage['buyercall'] || '{}');
allStorage[guid] = _.extend(getAll(), dict);
window.localStorage['buyercall'] = JSON.stringify(allStorage);
}
this.get = function (key) {
return getAll()[key];
};
this.set = function (key, value) {
var dict = getAll();
dict[key] = value;
setAll(dict);
};
};
var CallWidgetImage = Backbone.View.extend({
id: 'buyercall-widget-image',
template: `
<div id="buyercall-widget-image-close" class="buyercall-hide"></div>
`,
events: {
'click img': 'handleClick',
'click #buyercall-widget-image-close': 'handleClose',
},
initialize: function (options) {
this.widget = options.widget;
this.mobile = utils.isMobileDevice();
},
render: function () {
this.$el.html(this.template);
this.$el.css('z-index', '50000');
if (!this.el.parentNode)
return this;
},
updateSettings: function (imageUrl, settings) {
if (!imageUrl || !settings.typeImage) {
this.$el.find('img').remove();
}
if (this.mobile && !settings.showOnMobile){
$('#buyercall-widget-image').css('display','none');
}
if (this.mobile && settings.mobileOptimized){
$('#buyercall-widget-image').css('display','none');
}
if (imageUrl !== this.imageUrl) {
this.$el.find('img').remove();
}
if (imageUrl && settings.typeImage && !this.$el.find('img').length &&
this.widget.getState() == 'closed') {
this.$el.append(`<img src="${imageUrl}" />`);
}
this.imageUrl = imageUrl;
// Image manipulation
var translateX = settings.imageHorizontal - 50;
var translateY = 50 - settings.imageVertical;
var scale = settings.imageSize;
var transformImage = () => {
this.imageWidth = this.$('img').prop('naturalWidth');
this.imageHeight = this.$('img').prop('naturalHeight');
var newWidth = this.imageWidth * settings.imageSize / 100;
var newHeight = this.imageHeight * settings.imageSize / 100;
var parentHeight, parentWidth, position;
var container = this.el.parentElement;
if (container) {
if ($(container).hasClass('widget-preview')) {
parentHeight = $(container).innerHeight();
parentWidth = $(container).innerWidth();
position = 'absolute';
} else {
container = window;
parentHeight = container.innerHeight;
parentWidth = container.innerWidth;
position = 'fixed';
}
var left = ((parentWidth - newWidth) * settings.imageHorizontal / 100);
var top = ((parentHeight - newHeight) * settings.imageVertical / 100);
this.$el.find('img').css({
'width': newWidth + 'px',
});
this.$el.css({
'position': position,
'left': left + 'px',
'top': top + 'px',
'width': newWidth + 'px',
});
}
return this;
};
this.imageWidth = this.$('img').prop('naturalWidth');
if (!this.imageWidth) {
utils.afterImageLoad(this.imageUrl, transformImage);
} else {
transformImage();
}
return this;
},
hide: function () {
this.$el.hide();
},
show: function () {
this.$el.show();
},
handleClick: function (e) {
this.trigger('click');
},
handleClose: function (e) {
this.trigger('close');
},
});
var CallWidgetView = Backbone.View.extend({
events: {
'click .buyercall-widget-header': 'handleHeaderClick',
'click .buyercall-widget-collapse-button': 'handleCollapseClick',
'click #buyercall-talk-button': 'handleTalkClick',
'click #buyercall-submit-button': 'handleSubmitClick',
'click .buyercall-widget-hide-button': 'handleHideClick'
},
id: 'buyercall-widget',
className: 'state-closed',
animationShown: null,
previewMode: false,
imageWidth: 1,
$class: function (class_, condition) {
if (condition) {
this.$el.addClass(class_);
} else {
this.$el.removeClass(class_);
}
},
initialize: function (options) {
this.guid = options.guid;
this.settings = new WidgetModel();
this.mobile = utils.isMobileDevice();
this.setState(options.state || 'closed');
$.ajax(SETTINGS_URL + this.guid, {
method: 'GET',
success: (data) => {
this.settings.set(data);
// Only render after image has loaded
let imageUrl = this.getImageUrl();
utils.afterImageLoad(imageUrl, () => { this.renderSettings(); });
}
});
},
renderSettings: function () {
this.render();
this.listenTo(this.settings, 'change', () => {
this.widgets.forEach((widget) => {
widget.render();
});
this.updateSettings(this.settings.toJSON());
});
},
render: function () {
//if (this.mobile && !this.settings.get('showOnMobile')) {
//return;
//}
var widgetOptions = [
{ id: 'buyercall-before-call', template: beforeCallTemplate },
{ id: 'buyercall-unavailable', template: unavailableTemplate },
{ id: 'buyercall-inprogress', template: inProgressTemplate },
{ id: 'buyercall-error', template: errorTemplate },
{ id: 'buyercall-thankyou', template: thankYouTemplate },
{ id: 'buyercall-closed', template: closedTemplate }
];
this.widgets = [];
widgetOptions.forEach((opt, i) => {
var widget = new TemplateView({
model: this.settings,
template: opt.template,
id: opt.id
});
this.widgets.push(widget);
widget.$el.appendTo(this.$el);
});
this.imageView = new CallWidgetImage({
settings: this.settings,
widget: this,
});
clearInterval(this.imageInterval);
this.imageInterval = setInterval(() => {
if (this.el.parentNode && !this.imageView.el.parentNode) {
clearInterval(this.imageInterval);
this.imageView.render().$el.appendTo(this.el.parentNode);
this.imageView.updateSettings(this.getImageUrl(), this.settings.toJSON());
}
}, 200);
this.imageView.on('click', () => {
this.handleHeaderClick();
});
this.imageView.on('close', () => {
this.handleImageCloseClick();
});
this.updateSettings(this.settings.toJSON());
this.addButtonListener();
this.setupValidation();
if (this.hasType('tab') || this.hasType('image')) {
this.$el.show();
}
return this;
},
/**
* Show modal pop-up on button and link clicks for this widget.
*/
addButtonListener: function () {
if (!this.hasType('link') && !this.hasType('button')) {
return;
}
$(document.body).on('click', (evt) => {
var $button = $(evt.originalEvent.target).closest('.buyercall-button');
if ($button.length && $button.data('widget-guid') === this.guid) {
this.showLightbox();
}
});
},
setupValidation: function () {
this.$('#buyercall-before-call form').validate({
rules: {
'widget-lead-phone-number': {
phoneUS: true
}
},
messages: {},
highlight: function (element) {
$(element).parent().addClass('has-error');
},
unhighlight: function (element) {
$(element).parent().removeClass('has-error');
}
});
this.$('#buyercall-unavailable form').validate({
rules: {
'widget-unavailable-lead-phone-number': {
phoneUS: true
}
},
messages: {},
highlight: function (element) {
$(element).parent().addClass('has-error');
},
unhighlight: function (element) {
$(element).parent().removeClass('has-error');
}
});
},
updateSettings: function (settings) {
settings = _.extend({}, defaultSettings, settings);
// Animation
if (this.animationShown !== settings.animation) {
this.animationShown = settings.animation;
this.startAnimation();
}
// Header color
this.$header = this.$('.buyercall-widget-header');
this.$header.css('background-color', settings.color);
var luminance = utils.getLuminance(settings.color);
if (luminance < 0.7) {
this.$header.css('color', 'white');
} else {
this.$header.css('color', 'black');
}
// Position
this.$el.removeClass('top-right top-left bottom-right bottom-left');
this.$el.addClass(settings.position);
// Type
for (let type of ['button', 'link', 'tab', 'image']) {
this.$class(`type-${type}`, this.hasType(type));
}
// Tab
this.updateTabWidth(settings);
// Image
let imageUrl = this.getImageUrl();
this.imageView.updateSettings(imageUrl, settings);
// Repeat animation
this.scheduleAnimation();
// Image in-front of tab
this.$class('image-in-front', settings.imageInFront);
// Mobile optimize
if (this.settings.get('showOnMobile') && !this.settings.get('mobileOptimized')) {
this.$class('mobile', this.mobile);
} else if (this.settings.get('mobileOptimized') && !this.settings.get('showOnMobile')) {
this.$class('mobile-optimized', settings.mobileOptimized);
this.$class('mobile', this.mobile);
} else if (this.settings.get('mobileOptimized') && this.settings.get('showOnMobile')) {
this.$class('mobile-optimized', settings.mobileOptimized);
this.$class('mobile', this.mobile);
} else {
this.$class('no-show-no-opt', this.mobile);
// console.log('The buyercall widget has been turned off on mobile.')
}
// Trigger
let triggering = false;
if (settings.triggerBasedOnPage) {
for (let i = 0; i < settings.pages.length; i++) {
let page = settings.pages[i];
if (window.location.href === page) {
setTimeout(() => {
if (this.getState() === 'closed') {
this.setState('beforecall');
}
}, settings.triggerPageInterval * 1000);
triggering = true;
break;
}
}
}
if (!triggering && settings.triggerBasedOnTime) {
setTimeout(() => {
if (this.getState() === 'closed') {
this.setState('beforecall');
}
}, settings.triggerInterval * 1000);
}
},
updateTabWidth: function (settings) {
if (this.mobile && settings.mobileOptimized) {
return;
}
if (this.el.parentElement) {
var containerWidth = this.el.parentElement.clientWidth;
if (this.getState() === 'closed') {
this.$el.css(
'width',
(containerWidth * settings.tabWidth / 100) + 'px');
return;
}
}
this.$el.css('width', '360px');
},
scheduleAnimation: function () {
if (this.settings.get('repeatAnimation') && this.agentsAvailable()) {
clearInterval(this.repeat);
this.repeat = setInterval(() => this.startAnimation(), ANIMATION_INTERVAL * 1000);
}
},
shouldHideImage: function () {
var storage = new LocalStorage(this.guid),
hideDate = storage.get('image-hide-date');
if (!hideDate) {
return false;
}
if (new Date().getTime() - hideDate < 3600 * 1000) {
return true;
}
storage.set('image-hide-date', undefined);
return false;
},
saveHideImageTime: function () {
var storage = new LocalStorage(this.guid);
storage.set('image-hide-date', new Date().getTime());
},
getImageUrl: function () {
if (this.shouldHideImage()) {
return '';
}
return this.settings.getImageUrl(this.guid);
},
startAnimation: function () {
var that = this,
animation = this.settings.get('animation'),
type = this.settings.get('type');
if (this.animationRunning) {
return;
}
if (animation && (this.hasType('image') || this.hasType('tab')) && this.getState() == 'closed') {
let isMobile = this.settings.get('mobileOptimized') && this.mobile;
this.animationRunning = true;
// TODO: Get rid of this crutch
this.undelegateEvents();
animate(this.$el, {
isMobile: isMobile,
animation: animation,
duration: 1000,
callback: () => {
this.$el.css({
'top': '',
'bottom': '',
'left': '',
'right': '',
'transform': ''
});
this.animationRunning = false;
this.delegateEvents();
}
});
}
},
isLightbox: function () {
return this.$el.hasClass('buyercall-lightbox');
},
closeLightbox: function () {
document.body.style.overflow = document.body.getAttribute('data-old-overflow');
$('.buyercall-lightbox-mask').remove();
this.$el.removeClass('buyercall-lightbox').css({
'position': '',
'left': '',
'top': '',
'bottom': '',
'right': ''
});
},
showLightbox: function () {
var oldOverflow = '';
// Prevent scrolling the window
document.body.setAttribute('data-old-overflow', document.body.style.overflow);
document.body.style.overflow = 'hidden';
var mask = $('<div class="buyercall-lightbox-mask">').insertBefore(
this.$el
).click((evt) => { this.setState('closed'); });
if (!this.settings.get('transparentBackground')) {
mask.css('opacity', '0');
}
this.$el.addClass('buyercall-lightbox');
if (!this.agentsAvailable()) {
this.setState('unavailable');
} else {
this.setState('beforecall');
}
this.$el.css({
'position': 'fixed',
'left': ($(window).width() - this.$el.width()) / 2,
'top': ($(window).height() - this.$el.height()) / 2,
'bottom': 'auto',
'right': 'auto'
});
this.$el.show();
},
getClasses: function (re, invert) {
return this.el.className
.split(' ')
.filter((x) => re.test(x) ? !invert : !!invert);
},
hasType: function(type) {
return !!this.settings.get(utils.idToPropertyName(`type-${type}`));
},
setState: function (newState) {
if (this.getState() === newState) {
return;
}
var classes = this.getClasses(/^state-/, true);
classes.push(`state-${newState}`);
this.el.className = classes.join(' ');
this.updateTabWidth(this.settings.toJSON());
if (newState == 'beforecall' && this.imageView) {
this.imageView.hide();
}
if (newState == 'beforecall' && this.settings.get('alwaysModal') && !this.isLightbox()) {
this.showLightbox();
}
if ((newState == 'closed' || newState == 'hidden') && this.isLightbox()) {
this.closeLightbox();
}
if ((newState == 'closed' || newState == 'hidden') && this.imageView) {
this.imageView.show();
}
if (newState == 'hidden') {
// Unhide widget on hover
setTimeout(() => {
this.$('.buyercall-widget-header').one('mousemove', () => {
this.setState('closed');
});
}, 1000);
}
},
getState: function () {
var classes = this.getClasses(/^state-/);
if (classes.length) {
return classes[0].substr(6);
}
return null;
},
/**
* Determines whether any agents are currently available.
*/
agentsAvailable: function () {
return this.settings.get('agentsAvailable');
},
/**
* Check that all mandatory fields are filled.
*/
validateWidget: function() {
var form = null,
isValid = true;
if (this.getState() == 'beforecall') {
form = this.$('#buyercall-before-call form ');
} else if (this.getState() == 'unavailable') {
form = this.$('#buyercall-unavailable form');
}
if (form) {
isValid = form.valid();
}
return isValid;
},
/**
* Starts a Twilio call to the lead's phone number, which must be passed as
* part of the lead information. Additionally, the following fields are
* supported:
*
* phoneNumber - the lead's phone number
* firstName - the lead's first name
* lastName - the lead's last name
* emailAddress - the lead's email address
* question - the question the lead is calling about.
* ...any custom information you might want to include. This will be stored
* in the buyercall database.
*
* @param {object} settings - The lead information
*/
startCall: function (settings) {
var deferred = $.Deferred();
$.ajax(CALL_URL + '?guid=' + this.guid, {
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(settings)
}).done(function (result) {
// Is the call being established?
if (result.success === true) {
let connect = $.Deferred();
if (result.callId != undefined) {
let pollCounter = 0,
interval = setInterval(function () {
if (pollCounter > CALL_POLL_TIMEOUT) {
clearInterval(interval);
connect.reject(__(ERR_TOOLONG_MSG));
return;
}
pollCounter += 2;
$.ajax({
url: STATUS_URL + '/' + result.callId,
success: function (data) {
if (data.callConnect) {
clearInterval(interval);
connect.resolve();
} else if (data.error) {
clearInterval(interval);
connect.reject(__(ERR_GENERIC_MSG))
}
}
});
}, 2000);
}
deferred.resolve({
success: true,
connectPromise: connect.promise()
});
return;
}
var errorMsg = __(ERR_GENERIC_MSG);
if (result.code === 'ERR_NO_AGENTS') {
errorMsg = __(ERR_NO_AGENTS_MSG);
}
deferred.reject({
success: false,
errorMessage: errorMsg
});
}).fail(function () {
deferred.reject({
success: false,
errorMessage: __(ERR_GENERIC_MSG)
});
});
return deferred.promise();
},
handleHeaderClick: function (event) {
if (this.isLightbox()) {
return;
}
// Open or close the widget
switch (this.getState()) {
case 'closed':
case 'hidden':
// Are the agents available?
if (!this.agentsAvailable()) {
this.setState('unavailable');
} else {
this.setState('beforecall');
}
break;
// case 'beforecall':
default:
this.setState('closed');
break;
}
},
handleCollapseClick: function (e) {
this.setState('closed');
// Ensure that the custom image is not showing when the popup is closed
if (this.mobile && this.settings.get('mobileOptimized')) {
$('#buyercall-widget-image').css('display','none');
}
e.stopPropagation();
},
handleHideClick: function (e) {
this.setState('hidden');
e.stopImmediatePropagation();
},
handleTalkClick: function () {
if (!this.validateWidget()) {
return;
}
var settings = {
firstName: this.$('#widget-lead-first-name').val(),
lastName: this.$('#widget-lead-last-name').val(),
emailAddress: this.$('#widget-lead-email-address').val(),
phoneNumber: this.$('#widget-lead-phone-number').val(),
question: this.$('#widget-question').val(),
source: ""
};
this.startCall(settings).done((result) => {
this.setState('inprogress');
result.connectPromise.done(() => {
this.setState('closed');
}).fail((errorMessage) => {
this.setState('error');
this.$('#buyercall-error .buyercall-preamble').text(errorMessage);
});
}).fail((result) => {
this.setState('error');
this.$('#buyercall-error .buyercall-preamble').text(result.errorMessage);
});
},
handleSubmitClick: function () {
if (!this.validateWidget()) {
return;
}
var settings = {
firstName: this.$('#widget-unavailable-lead-first-name').val(),
lastName: this.$('#widget-unavailable-lead-last-name').val(),
emailAddress: this.$('#widget-unavailable-lead-email-address').val(),
phoneNumber: this.$('#widget-unavailable-lead-phone-number').val(),
question: this.$('#widget-unavailable-question').val(),
source: ""
};
$.ajax(SAVE_URL + '?guid=' + this.guid, {
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(settings),
success: (data) => {
this.setState('thankyou');
},
error: () => {
this.setState('error');
this.$('#buyercall-error .buyercall-preamble').text(__(ERR_GENERIC_MSG));
}
});
},
handleImageCloseClick: function (e) {
this.$('#buyercall-widget-image img').remove();
this.settings.set({
'customImage': '',
'defaultImage': ''
});
this.saveHideImageTime();
if (e) {
e.stopPropagation();
}
}
});
// An instance of the widget
var CallWidgetPreView = CallWidgetView.extend({
events: {},
previewMode: true,
className: 'state-closed preview-mode',
initialize: function (options) {
this.guid = options.guid;
this.settings = new WidgetModel();
this.mobile = utils.isMobileDevice();
this.setState(options.state || 'closed');
this.settings = options.settings;
this.renderSettings();
},
agentsAvailable: function () {
return true;
},
addButtonListener: function () {},
scheduleAnimation: function() {},
saveHideImageTime: function() {},
shouldHideImage: function() { return false; }
});
var TemplateView = Backbone.View.extend({
initialize: function (options) {
this.template = options.template;
this.render();
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
module.exports = { CallWidgetView, CallWidgetPreView };
// Export the widget view, in case the file is loaded externally
var global = Function('return this')();
global.CallWidgetView = CallWidgetView;