b765d66a38f9bc2265de2d6ed5989718924edf88
[dialpad.git] / js / salesforce / interaction.js
1 /*
2 * Salesforce.com Interaction Toolkit 40.0
3 * Copyright, 2015, salesforce.com, inc.
4 * All Rights Reserved
5 */
6
7 'use strict';
8
9 window.sforce = window.sforce || {};
10
11 sforce.interaction = (function() {
12   var apiVersion = 40;
13   var INTERACTION_API = 'interactionApi/';
14   var ENTITY_FEED_API = 'entityFeedApi/';
15   var frameOrigin = null;
16   var nonce = null;
17   var callbacks = {};
18   var GET_CALL_CENTER_SETTINGS = 'getCallCenterSettings';
19   var GET_SOFTPHONE_LAYOUT = 'getSoftphoneLayout';
20   var GET_PAGE_INFO = 'getPageInfo';
21   var SET_SOFTPHONE_HEIGHT = 'setSoftphoneHeight';
22   var SET_SOFTPHONE_WIDTH = 'setSoftphoneWidth';
23   var IS_IN_CONSOLE = 'isInConsole';
24   var SCREEN_POP = 'screenPop';
25   var SEARCH_AND_GET_SCREEN_POP_URL = 'searchAndGetScreenPopUrl';
26   var SEARCH_AND_SCREEN_POP = 'searchAndScreenPop';
27   var ENABLE_CLICK_TO_DIAL = 'enableClickToDial';
28   var DISABLE_CLICK_TO_DIAL = 'disableClickToDial';
29   var RUN_APEX_QUERY = 'runApex';
30   var SAVE_LOG = 'saveLog';
31   var SET_VISIBLE = 'setVisible';
32   var IS_VISIBLE = 'isVisible';
33   var AVAILABLE = 'available';
34   var CONNECT = 'connect';
35   var DISCONNECT = 'disconnect';
36   var REFRESH_OBJECT = 'refreshObject';
37   var RELOAD_FRAME = 'reloadFrame';
38   var REFRESH_PAGE = 'refreshPage';
39   var REFRESH_RELATED_LIST = 'refreshRelatedList';
40   var NOTIFY_INITIALIZATION_COMPLETE = 'notifyInitializationComplete';
41   var GET_DIRECTORY_NUMBERS = "getDirectoryNumbers";
42   var listeners = {onClickToDial:'onClickToDial', onFocus:'onFocus', onObjectUpdate:'onObjectUpdate'};
43   var methodId = 0;
44   var entityFeedApi = null;
45   var servedFromCanvas = false;
46
47   function isApiMessage(event, apiEndPoint) {
48     return event.data && event.data.indexOf(apiEndPoint) === 0;
49   }
50
51   function parseAuthParams(params) {
52     // initialize if sfdcIFrameOrigin and nonce are present
53     if (params['sfdcIFrameOrigin'] && params['nonce']) {
54       frameOrigin = params['sfdcIFrameOrigin'] ? params['sfdcIFrameOrigin'].toLowerCase():null;
55       nonce = params['nonce'];
56     } else {
57       // we may use canvas
58       if (typeof Sfdc !== "undefined" && Sfdc.canvas && Sfdc.canvas.client) {
59         var sr = Sfdc.canvas.client.signedrequest();
60         var parsedRequest;
61         if (sr) {
62           if (typeof sr === "string") {
63             parsedRequest = JSON.parse(sr);
64           } else {
65             // assume we're using OAuth
66             parsedRequest = sr;
67           }
68         }
69         if (parsedRequest) {
70           var environment;
71           if (parsedRequest.context) {
72             environment = parsedRequest.context.environment;
73           } else if (parsedRequest.payload) {
74             environment = parsedRequest.payload.environment;
75           }
76           if (environment && environment.parameters) {
77             frameOrigin = environment.parameters.sfdcIframeOrigin;
78             nonce = environment.parameters.nonce;
79             servedFromCanvas = environment.parameters.servedFromCanvas;
80           }
81         }
82       }
83     }
84   }
85
86   /**
87   * Process messages received from SFDC by executing callbacks, if any.
88   * The event object contains the following fields:
89   *      method: the API method that was called.
90   *      result: result returned from the call.
91   *      error: an error message if any errors were encountered.
92   */
93   function processPostMessage(event) {
94     var params;
95     try {
96       // Check if call is for entity feed
97       if (isApiMessage(event, ENTITY_FEED_API)) {
98         if (entityFeedApi && entityFeedApi.processPostMessage) {
99           params = entityFeedApi.processPostMessage(event);
100         }
101         if (!params) {
102           return;
103         }
104       } else if (isApiMessage(event, INTERACTION_API)) {
105         if (event.origin !== frameOrigin || !frameOrigin) {
106           // Only trust messages from the adapter frame
107           return;
108         }
109
110         var message = event.data.replace(INTERACTION_API, ''); // strip off API target
111         params = parseUrlQueryString(message);
112
113         // convert string true/false to boolean for methods that needs to return boolean values.
114         if (params && (params.result === 'true' || params.result === 'false')) {
115           params.result = params.result === 'true';
116         }
117       } else {
118         // return if postMessage is not targeted to interaction API
119         return;
120       }
121
122       // execute callbacks registered for the method called
123       for (var methodName in callbacks) {
124         if (callbacks.hasOwnProperty(methodName)) {
125           if (params.method === methodName) {
126             for (var i in callbacks[methodName]) {
127               callbacks[methodName][i](params);
128             }
129             if (!listeners[methodName]) {
130               delete callbacks[methodName];
131             }
132           }
133         }
134       }
135     } catch(e) {
136       consoleLog("Failed to process API response.");
137     }
138   }
139
140   /**
141   * Makes an API call to SFDC domain.
142   */
143   function doPostMessage(params, callback) {
144     if (callback) {
145       params.method = registerCallback(params.method, callback);
146     }
147
148     // add nonce to params
149     params.nonce = nonce;
150
151     // add version
152     params.apiVersion = apiVersion;
153
154     if (frameOrigin) {
155       var targetWindow = servedFromCanvas ? window.top : window.parent;
156       targetWindow.postMessage(INTERACTION_API + buildQueryString(params), frameOrigin);
157     }
158   }
159
160   function registerCallback(method, callback) {
161     if (listeners[method]) {
162       if (callbacks[method]) {
163         callbacks[method].push(callback);
164       } else {
165         callbacks[method] = [callback];
166       }
167     } else {
168       // API methods that are not listeners needs an ID in case they are call multiple times in an async manner.
169       method += '_' + methodId;
170       callbacks[method] = [callback];
171       methodId++;
172     }
173     return method;
174   }
175
176   /**
177   * Utility method to create a query string object.
178   */
179   function parseUrlQueryString(queryString) {
180     var params = {};
181     if (typeof queryString !== 'string') {
182       return params;
183     }
184
185     if (queryString.charAt(0) === '?') {
186       queryString = queryString.slice(1);
187     }
188
189     if (queryString.length === 0) {
190       return params;
191     }
192
193     var pairs = queryString.split('&');
194     for (var i = 0; i < pairs.length; i++) {
195       var pair = pairs[i].split('=');
196       params[pair[0]] = !!pair[1] ? decodeURIComponent(pair[1]) : null;
197     }
198
199     return params;
200   }
201
202   /**
203   * Utility method to build a query string from key/value object.
204   */
205   function buildQueryString(params) {
206     var qs = '';
207     for (var key in params) {
208       if (params.hasOwnProperty(key)) {
209         qs += key + '=' + encodeURIComponent(params[key]) + '&';
210       }
211     }
212     qs = qs.length > 0 ? qs.substr(0, qs.length-1) : qs;
213     return qs;
214   }
215
216   function consoleLog(message) {
217     if (window.console && console.log) {
218       console.log(message);
219     }
220   }
221
222   function jsonStringify(object) {
223     if (typeof Sfdc !== "undefined" && Sfdc.JSON) {
224       return Sfdc.JSON.stringify(object);
225     } else {
226       return JSON.stringify(object);
227     }
228   }
229
230   function jsonParse(json) {
231     if (typeof Sfdc !== "undefined" && Sfdc.JSON) {
232       return Sfdc.JSON.parse(json);
233     } else {
234       return JSON.parse(json);
235     }
236   }
237
238   /**
239   * Entity Feed API implementation.
240   */
241   function EntityFeedApi(params) {
242     var that = this;
243     var nonce = null;
244     var apiFrame;
245     var apiOrigin;
246     var readyQueue = [];
247     this.processPostMessage;
248
249     function processApiResponse(event) {
250       return decodeMessage(event);
251     }
252
253     function decodeMessage(event) {
254       if (isApiMessage(event, ENTITY_FEED_API)) {
255         // Decode message and check authenticity
256         var wrapper = jsonParse(event.data.substr(ENTITY_FEED_API.length));
257
258         // Check that message source is the API and that nonce is present unless it is an AVAILABLE broadcast
259         if (wrapper.message.fromApi && (wrapper.nonce === nonce || (!wrapper.nonce && wrapper.message.method === AVAILABLE))) {
260           return wrapper.message;
261         }
262       }
263       return null;
264     }
265
266     function doPostMessage(frames, targetOrigin, message, callback, connect) {
267       if (!nonce) {
268         consoleLog("API is not supported in this configuration.");
269         return;
270       }
271
272       // Register callback if any
273       if (callback) {
274         message.method = registerCallback(message.method, callback);
275       }
276
277       // Encode message
278       message.toApi = true;
279       message.version = apiVersion;
280       var messageContainer = {message: message, sourceFrameName: window.name};
281       if (connect) {
282         messageContainer.connect = true;
283       } else {
284         messageContainer.nonce = nonce;
285       }
286       var postData = ENTITY_FEED_API + jsonStringify(messageContainer);
287
288       for (var i = 0, len = frames.length; i < len; i++) {
289         frames[i].postMessage(postData, targetOrigin);
290       }
291     }
292
293     this.callApi = function(message, callback) {
294       if (apiFrame) {
295         doPostMessage([apiFrame], apiOrigin, message, callback, false);
296       } else {
297         readyQueue.push(function() {
298           that.callApi(message, callback);
299         });
300       }
301     };
302
303     this.reloadFrame = function() {
304       if (apiFrame) {
305         if (params.frameName) {
306           that.callApi({method: RELOAD_FRAME, objectId: params.id, frameName: params.frameName});
307         } else {
308           location.reload();
309         }
310       }
311     };
312
313     function initialize() {
314       nonce = params.entityFeedNonce;
315
316       that.processPostMessage = function(event) {
317         var message = decodeMessage(event);
318         if (message == null) {
319           return;
320         } else if (message.method === CONNECT && (!params.id || message.objectId === params.id)) { // Inline page frames do not have an id param
321           apiFrame = event.source;
322           apiOrigin = event.origin;
323           that.processPostMessage = processApiResponse;
324
325           for (var i = 0, len = readyQueue.length; i < len; i++) {
326             readyQueue[i]();
327           }
328           readyQueue = null;
329         } else if (message.method === AVAILABLE && message.objectId === params.id) {
330           // Detail page frame in console is notifying that API is available, try to connect
331           doPostMessage([event.source], '*', {method: CONNECT, objectId: params.id}, null, true);
332         }
333       };
334
335       var loadHandler = function() {
336         // Remove load handler
337         if (window.removeEventListener) {
338           window.removeEventListener("load", arguments.callee, false);
339         } else if (window.detachEvent) {
340           window.detachEvent("onload", arguments.callee);
341         }
342
343         // Search for api connection point.
344         var frames = [];
345         // Connect to current frame if api is available
346         if (typeof entityFeedPage != "undefined") {
347           frames.push(window);
348         } else {
349           // Attach to parent if VF custom publisher
350           frames.push(window.parent);
351
352           // Attach to siblings for console frames if this is not an inline VF page in the entity feed page
353           if (!params.isCEFP) {
354             for (var parentFrames = window.parent.frames, i = 0, len = parentFrames.length; i < len; i++) {
355               if (parentFrames[i] !== window.self) {
356                 frames.push(parentFrames[i]);
357               }
358             }
359           }
360         }
361         // Call frames to connect
362         doPostMessage(frames, '*', {method: CONNECT, objectId: params.id}, null, true);
363       };
364
365       var unloadHandler = function() {
366         // Remove unload handler
367         if (window.removeEventListener) {
368           window.removeEventListener("unload", arguments.callee, false);
369         } else if (window.detachEvent) {
370           window.detachEvent("onunload", arguments.callee);
371         }
372         if (apiFrame) {
373           doPostMessage([apiFrame], apiOrigin, {method: DISCONNECT}, null, false);
374           apiFrame = null;
375         }
376       };
377
378       if (window.addEventListener) {
379         window.addEventListener("load", loadHandler, false);
380         window.addEventListener("unload", unloadHandler, false);
381       } else if (window.attachEvent) {
382         window.attachEvent("onload", loadHandler);
383         window.attachEvent("onunload", unloadHandler);
384       }
385     };
386     initialize();
387   }
388
389   return {
390
391     /**
392     * Initializes API to listen for responses from SFDC.
393     */
394     initialize : function() {
395       // set sfdc frame origin and nonce needed to call API methods
396       var params = parseUrlQueryString(location.search);
397
398       parseAuthParams(params);
399
400       // initialize entity feed api
401       if (!entityFeedApi && params.entityFeedNonce && typeof window.postMessage !== "undefined") {
402         entityFeedApi = new EntityFeedApi(params);
403       }
404
405       if (frameOrigin || entityFeedApi) {
406         // attach postMessage event to handler
407         if (window.attachEvent) {
408           window.attachEvent('onmessage', processPostMessage);
409         } else {
410           window.addEventListener('message', processPostMessage, false);
411         }
412       }
413
414     },
415
416     /**
417     * Returns true if is in console, false otherwise
418     */
419     isInConsole : function (callback) {
420       doPostMessage({method:IS_IN_CONSOLE}, callback);
421     },
422
423     /**
424     * Screen pops to targetUrl and returns true if screen pop was successfully called, false otherwise.
425     * Parameter force must be a boolean. Set this value to true to force screen pop, i.e.: to force screen pop on an edit page.
426     */
427     screenPop : function (targetUrl, force, callback) {
428       doPostMessage({method:SCREEN_POP, targetUrl:targetUrl, force:!!force}, callback);
429     },
430
431     searchAndGetScreenPopUrl : function (searchParams, queryParams, callType, callback) {
432       doPostMessage({method:SEARCH_AND_GET_SCREEN_POP_URL, searchParams:searchParams, queryParams:queryParams, callType:callType}, callback);
433     },
434
435     searchAndScreenPop : function (searchParams, queryParams, callType, callback) {
436       doPostMessage({method:SEARCH_AND_SCREEN_POP, searchParams:searchParams, queryParams:queryParams, callType:callType}, callback);
437     },
438
439     /**
440     * Returns the current page info parameters: page Url, object Id (if applicable), object Name (if applicable), object (if applicable) as a JSON String.
441     */
442     getPageInfo : function (callback) {
443       doPostMessage({method:GET_PAGE_INFO}, callback);
444     },
445
446     /**
447     * Registers a callback to be fired when the page gets focused.
448     * When the callback is fired, it returns the current page info parameters: page Url, entity Id (if applicable), entity Name (if applicable) as a JSON String.
449     */
450     onFocus : function (callback) {
451       doPostMessage({method:listeners.onFocus}, callback);
452     },
453
454     /**
455     * Save object to database and return true if object was saved successfully, false otherwise.
456     * objectName is the API name of an object
457     * saveParams is a query string representing a key-value pair of object fields to save.
458     * Example:
459     *      // to save a new record
460     *      sforce.interaction.saveLog('Account', 'Name=Acme&Phone=4152125555', callback);
461     *      // to update a new record
462     *      sforce.interaction.saveLog('Account', 'Id=001D000000J6qIX&Name=UpdatedAcmeName', callback);
463     */
464     saveLog : function(objectName, saveParams, callback) {
465       doPostMessage({method:SAVE_LOG, objectName:objectName, saveParams:encodeURIComponent(saveParams)}, callback);
466     },
467
468     /**
469     * Runs an Apex method from a class with supplied parameters.
470     */
471     runApex : function(apexClass, methodName, methodParams, callback) {
472       doPostMessage({method:RUN_APEX_QUERY, apexClass:apexClass, methodName:methodName, methodParams:methodParams}, callback);
473     },
474
475     /**
476     * Returns true if widget was successfully shown or hidden, false otherwise.
477     * Parameter value must be a boolean.
478     * Parameter callback must be a function.
479     * If false is returned, an error message is also returned.
480     */
481     setVisible : function (value, callback) {
482       doPostMessage({method:SET_VISIBLE, value:value}, callback);
483     },
484
485     /**
486     * Returns true if widget is visible, false otherwise.
487     */
488     isVisible : function (callback) {
489       doPostMessage({method:IS_VISIBLE}, callback);
490     },
491
492     /**
493     * Returns true if page refresh is invoked, false otherwise.
494     */
495     refreshPage : function (callback) {
496       doPostMessage({method:REFRESH_PAGE}, callback);
497     },
498
499     /**
500     * Returns true if the related list with the given name is refreshed, false otherwise.
501     */
502     refreshRelatedList : function (listName, callback) {
503       doPostMessage({method:REFRESH_RELATED_LIST, listName:listName}, callback);
504     },
505
506     cti: {
507       /**
508       * Gets Call Center Settings.
509       */
510       getCallCenterSettings : function (callback) {
511         doPostMessage({method:GET_CALL_CENTER_SETTINGS}, callback);
512       },
513
514       /**
515       * Gets Softphone Layout.
516       */
517       getSoftphoneLayout : function (callback) {
518         doPostMessage({method:GET_SOFTPHONE_LAYOUT}, callback);
519       },
520
521       /**
522       * Sets softphone height. Height must be greater or equal than zero
523       */
524       setSoftphoneHeight : function (height, callback) {
525         doPostMessage({method:SET_SOFTPHONE_HEIGHT, height:height}, callback);
526       },
527
528       /**
529       * Sets softphone width. Width must be greater or equal than zero.
530       */
531       setSoftphoneWidth : function (width, callback) {
532         doPostMessage({method:SET_SOFTPHONE_WIDTH, width:width}, callback);
533       },
534
535       /**
536       * Enables click to dial.
537       */
538       enableClickToDial : function (callback) {
539         doPostMessage({method:ENABLE_CLICK_TO_DIAL}, callback);
540       },
541
542       /**
543       * Disables click to dial.
544       */
545       disableClickToDial : function (callback) {
546         doPostMessage({method:DISABLE_CLICK_TO_DIAL}, callback);
547       },
548
549       /**
550       * Registers callback to be fired when user clicks to dial.
551       */
552       onClickToDial : function (callback) {
553         doPostMessage({method:listeners.onClickToDial}, callback);
554       },
555
556       /**
557       * Notifies that the adapter url has been successfully loaded.
558       * Should be used if the standby url has been initialized.
559       */
560       notifyInitializationComplete: function() {
561         doPostMessage({method:NOTIFY_INITIALIZATION_COMPLETE});
562       },
563
564       /**
565       * Returns a list of phone numbers from a call center directory.
566       */
567       getDirectoryNumbers : function (isGlobal, callCenterName, callback, resultSetPage, resultSetPageSize) {
568         var params = {method:GET_DIRECTORY_NUMBERS, isGlobal: isGlobal};
569         if (callCenterName) {
570           params.callCenterName = callCenterName;
571         }
572         if (resultSetPage) {
573           params.resultSetPage = resultSetPage;
574         }
575         if (resultSetPageSize) {
576           params.resultSetPageSize = resultSetPageSize;
577         }
578         doPostMessage(params, callback);
579       }
580     },
581
582     /**
583     * Public API for Entity feed
584     */
585     entityFeed: {
586       /**
587       * Notifies that the object has been updated and its display need to be refreshed
588       */
589       refreshObject : function(objectId, refreshFields, refreshRelatedLists, refreshFeed, callback) {
590         entityFeedApi && entityFeedApi.callApi({method: REFRESH_OBJECT, objectId: objectId || params.id, refreshFields: refreshFields, refreshRelatedLists: refreshRelatedLists, refreshFeed: refreshFeed}, callback);
591       },
592
593       /**
594       * Registers a callback to be fired when the object has been updated.
595       */
596       onObjectUpdate : function(callback) {
597         entityFeedApi && entityFeedApi.callApi({method: listeners.onObjectUpdate}, callback);
598       },
599
600       /**
601       * Reloads the frame containing this page
602       */
603       reloadFrame : function() {
604         entityFeedApi && entityFeedApi.reloadFrame();
605       }
606     }
607   };
608 })();
609
610 sforce.interaction.initialize();