Source: ChatObserver.js

  1. /**
  2. * ChatObserver.js
  3. *
  4. * Created by Matthias Seemann on 17.01.2013.
  5. * Copyright (c) 2013 Visisoft GbR. All rights reserved.
  6. */
  7. define(['underscore', 'lib/UserAgent', 'api/ManagedError', 'api/errorCodes', 'api/configuration', 'lib/asyncLoadElement', 'lib/ajax', 'lib/devTools'],
  8. function(_, UserAgent, ManagedError, ErrorCode, Configuration, asyncLoadExternalSourceIntoElement, ajax, devTools)
  9. {
  10. /**
  11. * @type {ChatObserver}
  12. */
  13. var instance;
  14. /**
  15. * @type {Element}
  16. */
  17. var iFrameTag;
  18. /**
  19. * preparations for message reception completed
  20. * @type {Boolean}
  21. */
  22. var isArmed = false;
  23. /**
  24. *
  25. * @type {function[]}
  26. */
  27. var readyCallbacks = [];
  28. var LiveSupport = window[Configuration.configuration.GLOBAL_NAMESPACE];
  29. /**
  30. * @class ChatObserver
  31. * @constructor
  32. * @memberof LiveSupport.VisitorAPI
  33. * @description The central event publisher for all events the chat window sends.<br><br>
  34. * <h6>LIMITED SUPPORT:</h6> ChatObserver requires a <em>yalst Business</em> edition<br><br>
  35. * ChatObserver is not available on <em>mobile devices</em> where the chat runs in a dedicated browser tab
  36. * as touch-friendly single page web application. In this case you can use the
  37. * [ThemeApi ChatEventPublisher]{@link http://www.visisoft.de/theme_api/module-themeApi.html#.chatEventPublisher}.<br><br>
  38. * The chat window to be observed can be created by {@link LiveSupport.VisitorAPI.startLiveChat} or by
  39. * the chat button which is created by the regular <em>yalst</em> integration code.<br><br>
  40. * There can be just a single instance of ChatObserver per page. <em>It must be
  41. * accessed via {@link LiveSupport.VisitorAPI.ChatObserver.asyncSharedChatObserver}().</em><br><br>
  42. * IMPORTANT: Attempts to call this constructor will fail with an exception. (Singleton pattern)
  43. * <br><br>
  44. * <h6>LIMITED BROWSER SUPPORT:</h6>
  45. * <ul><li>will not work in Internet Explorer version 7 or older.</li>
  46. * <li>will not work in Google Chrome if third-party cookies are blocked by the user</li>
  47. * <li>in Internet Explorer 8 and 9 the schema (<tt>http</tt> or <tt>https</tt>) of the url in the
  48. * {@link LiveSupport.VisitorAPI.associateWithLiveSupportProduct} command must match the SSL capability of
  49. * your yalst product.</li>
  50. * <li>will not work with the touch-friendly chat for mobile devices. Instead inject JavaScript via user
  51. * theme and subscribe to `KeyStepPublisher`.</li>
  52. * </ul>
  53. * <br><br><strong>Throws</strong> {@link LiveSupport.ManagedError} in case readyCallback is not given and
  54. * <ul><li>the constructor is called more than once or</li>
  55. * <li>the browser is not supported (IE 7 and older)</li></ul>
  56. * @property {function()} onChatEvent Event handlers receive a single parameter
  57. * which is in case of success a
  58. * {@link LiveSupport.VisitorAPI.ChatObserver.Event} or a {@link LiveSupport.ManagedError} in
  59. * case of failure.
  60. */
  61. var ChatObserver = function()
  62. {
  63. this.onChatEvent = function(evt){};
  64. this.urlRegExp = new RegExp("^(https?:)?\/\/([a-z0-9-]+(\\.[a-z0-9-]+)+)([\\/?].+)?$");
  65. this.productOriginDomain = this.urlRegExp.exec(Configuration.configuration.PRODUCT_URL)[2];
  66. /**
  67. * @private
  68. * @param {MessageEvent} event
  69. */
  70. this.postMessageEventHandler = function(event)
  71. {
  72. if (!event.data || !(event.data in eventByXOriginMessageEventKey))
  73. {
  74. return;
  75. }
  76. var originDomain = this.urlRegExp.exec(event.origin)[2];
  77. if ((event.origin == location.href) || (originDomain == this.productOriginDomain))
  78. {
  79. this.onChatEvent(eventByXOriginMessageEventKey[event.data]);
  80. }
  81. };
  82. var sanityError;
  83. if (typeof instance == "object")
  84. {
  85. sanityError = new ManagedError(ErrorCode.ForbiddenSingletonConstructorError,
  86. "Invalid attempt to create another ChatObserver instance.");
  87. }
  88. var userAgent = UserAgent.sharedUserAgent();
  89. if (userAgent.isIE() && (userAgent.version() < 8.0))
  90. {
  91. sanityError = new ManagedError(ErrorCode.UnsupportedBrowser,
  92. "Method requires at least Internet Explorer 8");
  93. }
  94. if (sanityError)
  95. {
  96. if (readyCallbacks.length > 0)
  97. {
  98. while (readyCallbacks.length > 0)
  99. {
  100. readyCallbacks.shift().call(null, sanityError);
  101. }
  102. }
  103. else
  104. {
  105. throw sanityError;
  106. }
  107. return;
  108. }
  109. function onEventReceiverIFrameLoaded()
  110. {
  111. isArmed = true;
  112. while (readyCallbacks.length > 0)
  113. {
  114. readyCallbacks.shift().call(null, instance);
  115. }
  116. if (window.LiveSupport != undefined && window.LiveSupport.VisitorAPI != undefined
  117. && window.LiveSupport.VisitorAPI._registeredChatObserverIds instanceof Array)
  118. {
  119. /// origin ids written by yalst.js.php if loaded to create a chat button ("button != 'no'")
  120. ChatObserver.addRegisteredOriginIds(LiveSupport.VisitorAPI._registeredChatObserverIds);
  121. }
  122. }
  123. var shouldSendViaLocalStorage = (userAgent.isIE() && (userAgent.version() >= 9.0))
  124. || userAgent.isChrome() || userAgent.isNotBranded();
  125. var shouldUseJavascriptHandler = userAgent.isIE() && (userAgent.version() < 9.0);
  126. if (shouldSendViaLocalStorage || shouldUseJavascriptHandler)
  127. {
  128. /**
  129. * name attribute of the helper iFrame must be identically named in "msgframe.html"
  130. * @const
  131. * @default yeventreceiver
  132. * @type {String}
  133. */
  134. var FRAME_NAME = "yeventreceiver";
  135. if (document.getElementsByName(FRAME_NAME).length == 0)
  136. {
  137. iFrameTag = document.createElement("iframe");
  138. iFrameTag.name = FRAME_NAME;
  139. (iFrameTag.frameElement || iFrameTag).style.display = "none";
  140. (iFrameTag.frameElement || iFrameTag).style.cssText
  141. = "width: 0; height: 0; border: 0";
  142. /// find out if the chat will run under https or http
  143. /// so that the iFrame will be loaded with the right schema
  144. if (ajax.isCORSSupported() && ajax.isJsonSupported())
  145. {
  146. ajax.getJson(Configuration.configuration.PRODUCT_URL + "chat.api.php?cmd=hello&clientcap=1.3&site="
  147. + Configuration.configuration.PRODUCT_SITE
  148. , function(response, error)
  149. {
  150. var schema;
  151. if (error)
  152. {
  153. if (Configuration.configuration.DEBUG)
  154. {
  155. devTools.console.log("Chat API Ajax error: " + error.message);
  156. }
  157. schema = "https://";
  158. }
  159. schema = response.ssl ? "https://" : "http://";
  160. var iFrameUrl = Configuration.configuration.PRODUCT_URL.replace(/^.*?\/\//, schema)
  161. + "eventreceiver.html?" + encodeURIComponent(location.href);
  162. asyncLoadExternalSourceIntoElement(iFrameTag
  163. , iFrameUrl
  164. , onEventReceiverIFrameLoaded
  165. , document.body
  166. );
  167. }
  168. );
  169. }
  170. else
  171. {
  172. /// for old IEs
  173. // I assume that the url parameter of the associate command is already in the right schema
  174. var iFrameUrl = Configuration.configuration.PRODUCT_URL + "eventreceiver.html?"
  175. + encodeURIComponent(location.href);
  176. asyncLoadExternalSourceIntoElement(iFrameTag
  177. , iFrameUrl
  178. , onEventReceiverIFrameLoaded
  179. , document.body);
  180. }
  181. }
  182. else if (document.getElementsByName(FRAME_NAME).contentWindow)
  183. {
  184. isArmed = true;
  185. _.defer(function()
  186. {
  187. while (readyCallbacks.length > 0)
  188. {
  189. readyCallbacks.shift().call(null, instance);
  190. }
  191. });
  192. }
  193. }
  194. else
  195. {
  196. isArmed = true;
  197. _.defer(function()
  198. {
  199. while (readyCallbacks.length > 0)
  200. {
  201. readyCallbacks.shift().call(null, instance);
  202. }
  203. });
  204. }
  205. if (window.addEventListener)
  206. {
  207. window.addEventListener("message", _.bind(this.postMessageEventHandler, this), false);
  208. }
  209. else if (window.attachEvent)
  210. {
  211. window.attachEvent("onmessage", _.bind(this.postMessageEventHandler, this));
  212. }
  213. instance = this;
  214. };
  215. /**
  216. * observable events fired by the chat (popup) window
  217. * @enum {string} Event
  218. * @readonly
  219. * @memberof LiveSupport.VisitorAPI.ChatObserver
  220. */
  221. ChatObserver.Event = {
  222. /**
  223. * Event is fired when the (popup) window showing the start chat form appears.
  224. * Event is silenced when the start form is skipped by specifying `direct=true` as
  225. * additional parameter to {@link LiveSupport.VisitorAPI.startLiveChat}
  226. */
  227. open : 'open',
  228. /**
  229. * Event is fired when the chat is started.
  230. */
  231. start : 'start',
  232. /**
  233. * Event is fired when the operator joins the chat.
  234. */
  235. join : 'join',
  236. /**
  237. * Event is fired when the chat is finished. This event is only delivered if the chat is
  238. * closed by the operator or by the user through the inner close button.
  239. * It does not fire if the user quits the popup using the window's system close button or
  240. * closes the popup before the chat is started.
  241. */
  242. finish : 'finish'
  243. };
  244. /**
  245. * @private
  246. * @param {array} ids
  247. */
  248. ChatObserver.addRegisteredOriginIds = function(ids)
  249. {
  250. if (_.isArray(ids))
  251. {
  252. _(ids).forEach(ChatObserver.addOriginId);
  253. }
  254. };
  255. /**
  256. * Registers an origin id which is broadcast by the popup window together with
  257. * the particular chat events.
  258. * @param {string} id
  259. */
  260. ChatObserver.addOriginId = function(id)
  261. {
  262. if (iFrameTag && isArmed && instance)
  263. {
  264. /// contentWindow is null when called in JsTestDriver environment from the traditional
  265. /// yalst integration code....!!!
  266. iFrameTag.contentWindow.postMessage(JSON.stringify({addId : id}), "*");
  267. }
  268. };
  269. /**
  270. * mapper between traditional 'yevent_' strings and the ChatObserver.Event enumeration
  271. * @type {Object}
  272. * @private
  273. */
  274. var eventByXOriginMessageEventKey = {
  275. "yevent_chatwindowopen" : ChatObserver.Event.open
  276. , "yevent_chatstarted" : ChatObserver.Event.start
  277. , "yevent_chatrunning" : ChatObserver.Event.join
  278. , "yevent_chatend" : ChatObserver.Event.finish
  279. };
  280. /**
  281. * Accesses and creates the single ChatObserver object.<br><br>
  282. * If called for the first time depending on the user agent this method may load additional external
  283. * resources to receive chat events. When this setup is complete the callback will be invoked
  284. * with the ChatObserver object as parameter or in case of an error an {@link LiveSupport.ManagedError} object.
  285. * @param {function} [resultCallback] called when ChatObserver is ready
  286. * to receive and re-publish chat window events. It may receive an Error object in case of an error the single
  287. * ChatObserver instance otherwise.
  288. * @memberof LiveSupport.VisitorAPI.ChatObserver
  289. * @example
  290. LiveSupport.VisitorAPI.ChatObserver.asyncSharedChatObserver(function(chatObserverOrError){
  291. if (chatObserverOrError instanceof Error){
  292. alert(chatObserverOrError.message);
  293. }
  294. else{
  295. chatObserverOrError.onChatEvent = chatEventHandler;
  296. startButton.removeAttribute("disabled");
  297. }
  298. });
  299. startButton.setAttribute("disabled", "disabled");
  300. function chatEventHandler(evtOrError){
  301. if (evtOrError instanceof LiveSupport.ManagedError){
  302. alert(evtOrError.message);
  303. return;
  304. }
  305. if (evtOrError == LiveSupport.VisitorAPI.ChatObserver.Event.join){
  306. startButton.style.visibility = "hidden";
  307. }
  308. else{
  309. startButton.style.visibility = "visible";
  310. }
  311. }
  312. */
  313. ChatObserver.asyncSharedChatObserver = /** @param {function} resultCallback */ function(resultCallback)
  314. {
  315. var callback = /** @param {LiveSupport.VisitorAPI.ChatObserver|Error} observerOrError */ function(observerOrError){};
  316. if (typeof resultCallback == "function")
  317. {
  318. callback = resultCallback;
  319. }
  320. if (typeof instance == "object")
  321. {
  322. _.defer(_.bind(callback, null, instance));
  323. }
  324. else if (UserAgent.sharedUserAgent().isIE() && (UserAgent.sharedUserAgent().version() < 8.0))
  325. {
  326. _.defer(_.bind(callback, null, new ManagedError(ErrorCode.UnsupportedBrowser,
  327. "Method requires at least Internet Explorer 8")));
  328. }
  329. else if (!Configuration.configuration.IS_ASSOCIATED)
  330. {
  331. _.defer(_.bind(callback, null, new ManagedError(ErrorCode.ServerNotYetAssociated
  332. , "Illegal API use. Attempt to call the method 'asyncSharedChatObserver'" +
  333. " before associated to a live support server.")));
  334. }
  335. else
  336. {
  337. readyCallbacks.push(callback);
  338. new ChatObserver();
  339. }
  340. };
  341. return ChatObserver;
  342. });