/**
* ChatObserver.js
*
* Created by Matthias Seemann on 17.01.2013.
* Copyright (c) 2013 Visisoft GbR. All rights reserved.
*/
define(['underscore', 'lib/UserAgent', 'api/ManagedError', 'api/errorCodes', 'api/configuration', 'lib/asyncLoadElement', 'lib/ajax', 'lib/devTools'],
function(_, UserAgent, ManagedError, ErrorCode, Configuration, asyncLoadExternalSourceIntoElement, ajax, devTools)
{
/**
* @type {ChatObserver}
*/
var instance;
/**
* @type {Element}
*/
var iFrameTag;
/**
* preparations for message reception completed
* @type {Boolean}
*/
var isArmed = false;
/**
*
* @type {function[]}
*/
var readyCallbacks = [];
var LiveSupport = window[Configuration.configuration.GLOBAL_NAMESPACE];
/**
* @class ChatObserver
* @constructor
* @memberof LiveSupport.VisitorAPI
* @description The central event publisher for all events the chat window sends.<br><br>
* <h6>LIMITED SUPPORT:</h6> ChatObserver requires a <em>yalst Business</em> edition<br><br>
* ChatObserver is not available on <em>mobile devices</em> where the chat runs in a dedicated browser tab
* as touch-friendly single page web application. In this case you can use the
* [ThemeApi ChatEventPublisher]{@link http://www.visisoft.de/theme_api/module-themeApi.html#.chatEventPublisher}.<br><br>
* The chat window to be observed can be created by {@link LiveSupport.VisitorAPI.startLiveChat} or by
* the chat button which is created by the regular <em>yalst</em> integration code.<br><br>
* There can be just a single instance of ChatObserver per page. <em>It must be
* accessed via {@link LiveSupport.VisitorAPI.ChatObserver.asyncSharedChatObserver}().</em><br><br>
* IMPORTANT: Attempts to call this constructor will fail with an exception. (Singleton pattern)
* <br><br>
* <h6>LIMITED BROWSER SUPPORT:</h6>
* <ul><li>will not work in Internet Explorer version 7 or older.</li>
* <li>will not work in Google Chrome if third-party cookies are blocked by the user</li>
* <li>in Internet Explorer 8 and 9 the schema (<tt>http</tt> or <tt>https</tt>) of the url in the
* {@link LiveSupport.VisitorAPI.associateWithLiveSupportProduct} command must match the SSL capability of
* your yalst product.</li>
* <li>will not work with the touch-friendly chat for mobile devices. Instead inject JavaScript via user
* theme and subscribe to `KeyStepPublisher`.</li>
* </ul>
* <br><br><strong>Throws</strong> {@link LiveSupport.ManagedError} in case readyCallback is not given and
* <ul><li>the constructor is called more than once or</li>
* <li>the browser is not supported (IE 7 and older)</li></ul>
* @property {function()} onChatEvent Event handlers receive a single parameter
* which is in case of success a
* {@link LiveSupport.VisitorAPI.ChatObserver.Event} or a {@link LiveSupport.ManagedError} in
* case of failure.
*/
var ChatObserver = function()
{
this.onChatEvent = function(evt){};
this.urlRegExp = new RegExp("^(https?:)?\/\/([a-z0-9-]+(\\.[a-z0-9-]+)+)([\\/?].+)?$");
this.productOriginDomain = this.urlRegExp.exec(Configuration.configuration.PRODUCT_URL)[2];
/**
* @private
* @param {MessageEvent} event
*/
this.postMessageEventHandler = function(event)
{
if (!event.data || !(event.data in eventByXOriginMessageEventKey))
{
return;
}
var originDomain = this.urlRegExp.exec(event.origin)[2];
if ((event.origin == location.href) || (originDomain == this.productOriginDomain))
{
this.onChatEvent(eventByXOriginMessageEventKey[event.data]);
}
};
var sanityError;
if (typeof instance == "object")
{
sanityError = new ManagedError(ErrorCode.ForbiddenSingletonConstructorError,
"Invalid attempt to create another ChatObserver instance.");
}
var userAgent = UserAgent.sharedUserAgent();
if (userAgent.isIE() && (userAgent.version() < 8.0))
{
sanityError = new ManagedError(ErrorCode.UnsupportedBrowser,
"Method requires at least Internet Explorer 8");
}
if (sanityError)
{
if (readyCallbacks.length > 0)
{
while (readyCallbacks.length > 0)
{
readyCallbacks.shift().call(null, sanityError);
}
}
else
{
throw sanityError;
}
return;
}
function onEventReceiverIFrameLoaded()
{
isArmed = true;
while (readyCallbacks.length > 0)
{
readyCallbacks.shift().call(null, instance);
}
if (window.LiveSupport != undefined && window.LiveSupport.VisitorAPI != undefined
&& window.LiveSupport.VisitorAPI._registeredChatObserverIds instanceof Array)
{
/// origin ids written by yalst.js.php if loaded to create a chat button ("button != 'no'")
ChatObserver.addRegisteredOriginIds(LiveSupport.VisitorAPI._registeredChatObserverIds);
}
}
var shouldSendViaLocalStorage = (userAgent.isIE() && (userAgent.version() >= 9.0))
|| userAgent.isChrome() || userAgent.isNotBranded();
var shouldUseJavascriptHandler = userAgent.isIE() && (userAgent.version() < 9.0);
if (shouldSendViaLocalStorage || shouldUseJavascriptHandler)
{
/**
* name attribute of the helper iFrame must be identically named in "msgframe.html"
* @const
* @default yeventreceiver
* @type {String}
*/
var FRAME_NAME = "yeventreceiver";
if (document.getElementsByName(FRAME_NAME).length == 0)
{
iFrameTag = document.createElement("iframe");
iFrameTag.name = FRAME_NAME;
(iFrameTag.frameElement || iFrameTag).style.display = "none";
(iFrameTag.frameElement || iFrameTag).style.cssText
= "width: 0; height: 0; border: 0";
/// find out if the chat will run under https or http
/// so that the iFrame will be loaded with the right schema
if (ajax.isCORSSupported() && ajax.isJsonSupported())
{
ajax.getJson(Configuration.configuration.PRODUCT_URL + "chat.api.php?cmd=hello&clientcap=1.3&site="
+ Configuration.configuration.PRODUCT_SITE
, function(response, error)
{
var schema;
if (error)
{
if (Configuration.configuration.DEBUG)
{
devTools.console.log("Chat API Ajax error: " + error.message);
}
schema = "https://";
}
schema = response.ssl ? "https://" : "http://";
var iFrameUrl = Configuration.configuration.PRODUCT_URL.replace(/^.*?\/\//, schema)
+ "eventreceiver.html?" + encodeURIComponent(location.href);
asyncLoadExternalSourceIntoElement(iFrameTag
, iFrameUrl
, onEventReceiverIFrameLoaded
, document.body
);
}
);
}
else
{
/// for old IEs
// I assume that the url parameter of the associate command is already in the right schema
var iFrameUrl = Configuration.configuration.PRODUCT_URL + "eventreceiver.html?"
+ encodeURIComponent(location.href);
asyncLoadExternalSourceIntoElement(iFrameTag
, iFrameUrl
, onEventReceiverIFrameLoaded
, document.body);
}
}
else if (document.getElementsByName(FRAME_NAME).contentWindow)
{
isArmed = true;
_.defer(function()
{
while (readyCallbacks.length > 0)
{
readyCallbacks.shift().call(null, instance);
}
});
}
}
else
{
isArmed = true;
_.defer(function()
{
while (readyCallbacks.length > 0)
{
readyCallbacks.shift().call(null, instance);
}
});
}
if (window.addEventListener)
{
window.addEventListener("message", _.bind(this.postMessageEventHandler, this), false);
}
else if (window.attachEvent)
{
window.attachEvent("onmessage", _.bind(this.postMessageEventHandler, this));
}
instance = this;
};
/**
* observable events fired by the chat (popup) window
* @enum {string} Event
* @readonly
* @memberof LiveSupport.VisitorAPI.ChatObserver
*/
ChatObserver.Event = {
/**
* Event is fired when the (popup) window showing the start chat form appears.
* Event is silenced when the start form is skipped by specifying `direct=true` as
* additional parameter to {@link LiveSupport.VisitorAPI.startLiveChat}
*/
open : 'open',
/**
* Event is fired when the chat is started.
*/
start : 'start',
/**
* Event is fired when the operator joins the chat.
*/
join : 'join',
/**
* Event is fired when the chat is finished. This event is only delivered if the chat is
* closed by the operator or by the user through the inner close button.
* It does not fire if the user quits the popup using the window's system close button or
* closes the popup before the chat is started.
*/
finish : 'finish'
};
/**
* @private
* @param {array} ids
*/
ChatObserver.addRegisteredOriginIds = function(ids)
{
if (_.isArray(ids))
{
_(ids).forEach(ChatObserver.addOriginId);
}
};
/**
* Registers an origin id which is broadcast by the popup window together with
* the particular chat events.
* @param {string} id
*/
ChatObserver.addOriginId = function(id)
{
if (iFrameTag && isArmed && instance)
{
/// contentWindow is null when called in JsTestDriver environment from the traditional
/// yalst integration code....!!!
iFrameTag.contentWindow.postMessage(JSON.stringify({addId : id}), "*");
}
};
/**
* mapper between traditional 'yevent_' strings and the ChatObserver.Event enumeration
* @type {Object}
* @private
*/
var eventByXOriginMessageEventKey = {
"yevent_chatwindowopen" : ChatObserver.Event.open
, "yevent_chatstarted" : ChatObserver.Event.start
, "yevent_chatrunning" : ChatObserver.Event.join
, "yevent_chatend" : ChatObserver.Event.finish
};
/**
* Accesses and creates the single ChatObserver object.<br><br>
* If called for the first time depending on the user agent this method may load additional external
* resources to receive chat events. When this setup is complete the callback will be invoked
* with the ChatObserver object as parameter or in case of an error an {@link LiveSupport.ManagedError} object.
* @param {function} [resultCallback] called when ChatObserver is ready
* to receive and re-publish chat window events. It may receive an Error object in case of an error the single
* ChatObserver instance otherwise.
* @memberof LiveSupport.VisitorAPI.ChatObserver
* @example
LiveSupport.VisitorAPI.ChatObserver.asyncSharedChatObserver(function(chatObserverOrError){
if (chatObserverOrError instanceof Error){
alert(chatObserverOrError.message);
}
else{
chatObserverOrError.onChatEvent = chatEventHandler;
startButton.removeAttribute("disabled");
}
});
startButton.setAttribute("disabled", "disabled");
function chatEventHandler(evtOrError){
if (evtOrError instanceof LiveSupport.ManagedError){
alert(evtOrError.message);
return;
}
if (evtOrError == LiveSupport.VisitorAPI.ChatObserver.Event.join){
startButton.style.visibility = "hidden";
}
else{
startButton.style.visibility = "visible";
}
}
*/
ChatObserver.asyncSharedChatObserver = /** @param {function} resultCallback */ function(resultCallback)
{
var callback = /** @param {LiveSupport.VisitorAPI.ChatObserver|Error} observerOrError */ function(observerOrError){};
if (typeof resultCallback == "function")
{
callback = resultCallback;
}
if (typeof instance == "object")
{
_.defer(_.bind(callback, null, instance));
}
else if (UserAgent.sharedUserAgent().isIE() && (UserAgent.sharedUserAgent().version() < 8.0))
{
_.defer(_.bind(callback, null, new ManagedError(ErrorCode.UnsupportedBrowser,
"Method requires at least Internet Explorer 8")));
}
else if (!Configuration.configuration.IS_ASSOCIATED)
{
_.defer(_.bind(callback, null, new ManagedError(ErrorCode.ServerNotYetAssociated
, "Illegal API use. Attempt to call the method 'asyncSharedChatObserver'" +
" before associated to a live support server.")));
}
else
{
readyCallbacks.push(callback);
new ChatObserver();
}
};
return ChatObserver;
});