Source: ChatObserver.js

/**
 * 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'],
	function(_, UserAgent, ManagedError, ErrorCode, Configuration, asyncLoadExternalSourceIntoElement)
{
	/**
	 * @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];

	/**
	 * 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>
	 * The chat window to be observed can be created by {@link LiveSupport.VisitorAPI.startLiveChat} or 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>
	 * </ul>
	 *
	 * @class ChatObserver
	 * @constructor
	 * @memberof LiveSupport.VisitorAPI
	 * @throws {LiveSupport.ManagedError} in case if 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;
		}

		var shouldSendViaLocalStorage = (userAgent.isIE() && (userAgent.version() >= 9.0))
													|| userAgent.isChrome() || userAgent.isNotBranded();
		var shouldUseJavascriptHandler = userAgent.isIE() && (userAgent.version() < 9.0);

		if (shouldSendViaLocalStorage || shouldUseJavascriptHandler)
		{
			/**
			 * those origin ids written by yalst.js.php if no Visitor API is yet loaded
			 * @type {*|Array}
			 * @private
			 */
			LiveSupport.VisitorAPI._registeredChatObserverIds =
				LiveSupport.VisitorAPI._registeredChatObserverIds || [];

			/**
			 * 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";

				asyncLoadExternalSourceIntoElement(iFrameTag
					, LiveSupport.VisitorAPI.configuration.PRODUCT_URL	+ "eventreceiver.html?"
						+ encodeURIComponent(location.href)
					, function()
					{
						isArmed = true;

						while (readyCallbacks.length > 0)
						{
							readyCallbacks.shift().call(null, instance);
						}

						ChatObserver.addRegisteredOriginIds(LiveSupport.VisitorAPI._registeredChatObserverIds);
					}
					, 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}), "*");
		}
		else
		{
			LiveSupport.VisitorAPI._registeredChatObserverIds =
				LiveSupport.VisitorAPI._registeredChatObserverIds || [];

			LiveSupport.VisitorAPI._registeredChatObserverIds.push(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;
});