Built xcally-motion-dialpad from commit 141221b.|1.0.96
[dialpad.git] / js / salesforce / interaction.js
1 /*
2  * Copyright 2017 salesforce.com, inc.
3  * All Rights Reserved
4  * Company Confidential
5  */
6
7 'use strict';
8
9 window.sforce = window.sforce || {};
10
11 sforce.interaction = (function() {
12     var apiVersion = 46;
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();