2 * Copyright 2017 salesforce.com, inc.
9 window.sforce = window.sforce || {};
11 sforce.interaction = (function() {
13 var INTERACTION_API = 'interactionApi/';
14 var ENTITY_FEED_API = 'entityFeedApi/';
15 var frameOrigin = null;
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'};
44 var entityFeedApi = null;
45 var servedFromCanvas = false;
47 function isApiMessage(event, apiEndPoint) {
48 return event.data && event.data.indexOf(apiEndPoint) === 0;
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'];
58 if (typeof Sfdc !== "undefined" && Sfdc.canvas && Sfdc.canvas.client) {
59 var sr = Sfdc.canvas.client.signedrequest();
62 if (typeof sr === "string") {
63 parsedRequest = JSON.parse(sr);
65 // assume we're using OAuth
71 if (parsedRequest.context) {
72 environment = parsedRequest.context.environment;
73 } else if (parsedRequest.payload) {
74 environment = parsedRequest.payload.environment;
76 if (environment && environment.parameters) {
77 frameOrigin = environment.parameters.sfdcIframeOrigin;
78 nonce = environment.parameters.nonce;
79 servedFromCanvas = environment.parameters.servedFromCanvas;
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.
93 function processPostMessage(event) {
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);
104 } else if (isApiMessage(event, INTERACTION_API)) {
105 if (event.origin !== frameOrigin || !frameOrigin) {
106 // Only trust messages from the adapter frame
110 var message = event.data.replace(INTERACTION_API, ''); // strip off API target
111 params = parseUrlQueryString(message);
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';
118 // return if postMessage is not targeted to interaction API
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);
129 if (!listeners[methodName]) {
130 delete callbacks[methodName];
136 //consoleLog("Failed to process API response.");
141 * Makes an API call to SFDC domain.
143 function doPostMessage(params, callback) {
145 params.method = registerCallback(params.method, callback);
148 // add nonce to params
149 params.nonce = nonce;
152 params.apiVersion = apiVersion;
155 var targetWindow = servedFromCanvas ? window.top : window.parent;
156 targetWindow.postMessage(INTERACTION_API + buildQueryString(params), frameOrigin);
160 function registerCallback(method, callback) {
161 if (listeners[method]) {
162 if (callbacks[method]) {
163 callbacks[method].push(callback);
165 callbacks[method] = [callback];
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];
177 * Utility method to create a query string object.
179 function parseUrlQueryString(queryString) {
181 if (typeof queryString !== 'string') {
185 if (queryString.charAt(0) === '?') {
186 queryString = queryString.slice(1);
189 if (queryString.length === 0) {
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;
203 * Utility method to build a query string from key/value object.
205 function buildQueryString(params) {
207 for (var key in params) {
208 if (params.hasOwnProperty(key)) {
209 qs += key + '=' + encodeURIComponent(params[key]) + '&';
212 qs = qs.length > 0 ? qs.substr(0, qs.length-1) : qs;
216 function consoleLog(message) {
217 if (window.console && console.log) {
218 console.log(message);
222 function jsonStringify(object) {
223 if (typeof Sfdc !== "undefined" && Sfdc.JSON) {
224 return Sfdc.JSON.stringify(object);
226 return JSON.stringify(object);
230 function jsonParse(json) {
231 if (typeof Sfdc !== "undefined" && Sfdc.JSON) {
232 return Sfdc.JSON.parse(json);
234 return JSON.parse(json);
239 * Entity Feed API implementation.
241 function EntityFeedApi(params) {
247 this.processPostMessage;
249 function processApiResponse(event) {
250 return decodeMessage(event);
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));
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;
266 function doPostMessage(frames, targetOrigin, message, callback, connect) {
268 consoleLog("API is not supported in this configuration.");
272 // Register callback if any
274 message.method = registerCallback(message.method, callback);
278 message.toApi = true;
279 message.version = apiVersion;
280 var messageContainer = {message: message, sourceFrameName: window.name};
282 messageContainer.connect = true;
284 messageContainer.nonce = nonce;
286 var postData = ENTITY_FEED_API + jsonStringify(messageContainer);
288 for (var i = 0, len = frames.length; i < len; i++) {
289 frames[i].postMessage(postData, targetOrigin);
293 this.callApi = function(message, callback) {
295 doPostMessage([apiFrame], apiOrigin, message, callback, false);
297 readyQueue.push(function() {
298 that.callApi(message, callback);
303 this.reloadFrame = function() {
305 if (params.frameName) {
306 that.callApi({method: RELOAD_FRAME, objectId: params.id, frameName: params.frameName});
313 function initialize() {
314 nonce = params.entityFeedNonce;
316 that.processPostMessage = function(event) {
317 var message = decodeMessage(event);
318 if (message == null) {
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;
325 for (var i = 0, len = readyQueue.length; i < len; i++) {
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);
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);
343 // Search for api connection point.
345 // Connect to current frame if api is available
346 if (typeof entityFeedPage != "undefined") {
349 // Attach to parent if VF custom publisher
350 frames.push(window.parent);
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]);
361 // Call frames to connect
362 doPostMessage(frames, '*', {method: CONNECT, objectId: params.id}, null, true);
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);
373 doPostMessage([apiFrame], apiOrigin, {method: DISCONNECT}, null, false);
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);
392 * Initializes API to listen for responses from SFDC.
394 initialize : function() {
395 // set sfdc frame origin and nonce needed to call API methods
396 var params = parseUrlQueryString(location.search);
398 parseAuthParams(params);
400 // initialize entity feed api
401 if (!entityFeedApi && params.entityFeedNonce && typeof window.postMessage !== "undefined") {
402 entityFeedApi = new EntityFeedApi(params);
405 if (frameOrigin || entityFeedApi) {
406 // attach postMessage event to handler
407 if (window.attachEvent) {
408 window.attachEvent('onmessage', processPostMessage);
410 window.addEventListener('message', processPostMessage, false);
417 * Returns true if is in console, false otherwise
419 isInConsole : function (callback) {
420 doPostMessage({method:IS_IN_CONSOLE}, callback);
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.
427 screenPop : function (targetUrl, force, callback) {
428 doPostMessage({method:SCREEN_POP, targetUrl:targetUrl, force:!!force}, callback);
431 searchAndGetScreenPopUrl : function (searchParams, queryParams, callType, callback) {
432 doPostMessage({method:SEARCH_AND_GET_SCREEN_POP_URL, searchParams:searchParams, queryParams:queryParams, callType:callType}, callback);
435 searchAndScreenPop : function (searchParams, queryParams, callType, callback) {
436 doPostMessage({method:SEARCH_AND_SCREEN_POP, searchParams:searchParams, queryParams:queryParams, callType:callType}, callback);
440 * Returns the current page info parameters: page Url, object Id (if applicable), object Name (if applicable), object (if applicable) as a JSON String.
442 getPageInfo : function (callback) {
443 doPostMessage({method:GET_PAGE_INFO}, callback);
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.
450 onFocus : function (callback) {
451 doPostMessage({method:listeners.onFocus}, callback);
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.
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);
464 saveLog : function(objectName, saveParams, callback) {
465 doPostMessage({method:SAVE_LOG, objectName:objectName, saveParams:encodeURIComponent(saveParams)}, callback);
469 * Runs an Apex method from a class with supplied parameters.
471 runApex : function(apexClass, methodName, methodParams, callback) {
472 doPostMessage({method:RUN_APEX_QUERY, apexClass:apexClass, methodName:methodName, methodParams:methodParams}, callback);
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.
481 setVisible : function (value, callback) {
482 doPostMessage({method:SET_VISIBLE, value:value}, callback);
486 * Returns true if widget is visible, false otherwise.
488 isVisible : function (callback) {
489 doPostMessage({method:IS_VISIBLE}, callback);
493 * Returns true if page refresh is invoked, false otherwise.
495 refreshPage : function (callback) {
496 doPostMessage({method:REFRESH_PAGE}, callback);
500 * Returns true if the related list with the given name is refreshed, false otherwise.
502 refreshRelatedList : function (listName, callback) {
503 doPostMessage({method:REFRESH_RELATED_LIST, listName:listName}, callback);
508 * Gets Call Center Settings.
510 getCallCenterSettings : function (callback) {
511 doPostMessage({method:GET_CALL_CENTER_SETTINGS}, callback);
515 * Gets Softphone Layout.
517 getSoftphoneLayout : function (callback) {
518 doPostMessage({method:GET_SOFTPHONE_LAYOUT}, callback);
522 * Sets softphone height. Height must be greater or equal than zero
524 setSoftphoneHeight : function (height, callback) {
525 doPostMessage({method:SET_SOFTPHONE_HEIGHT, height:height}, callback);
529 * Sets softphone width. Width must be greater or equal than zero.
531 setSoftphoneWidth : function (width, callback) {
532 doPostMessage({method:SET_SOFTPHONE_WIDTH, width:width}, callback);
536 * Enables click to dial.
538 enableClickToDial : function (callback) {
539 doPostMessage({method:ENABLE_CLICK_TO_DIAL}, callback);
543 * Disables click to dial.
545 disableClickToDial : function (callback) {
546 doPostMessage({method:DISABLE_CLICK_TO_DIAL}, callback);
550 * Registers callback to be fired when user clicks to dial.
552 onClickToDial : function (callback) {
553 doPostMessage({method:listeners.onClickToDial}, callback);
557 * Notifies that the adapter url has been successfully loaded.
558 * Should be used if the standby url has been initialized.
560 notifyInitializationComplete: function() {
561 doPostMessage({method:NOTIFY_INITIALIZATION_COMPLETE});
565 * Returns a list of phone numbers from a call center directory.
567 getDirectoryNumbers : function (isGlobal, callCenterName, callback, resultSetPage, resultSetPageSize) {
568 var params = {method:GET_DIRECTORY_NUMBERS, isGlobal: isGlobal};
569 if (callCenterName) {
570 params.callCenterName = callCenterName;
573 params.resultSetPage = resultSetPage;
575 if (resultSetPageSize) {
576 params.resultSetPageSize = resultSetPageSize;
578 doPostMessage(params, callback);
583 * Public API for Entity feed
587 * Notifies that the object has been updated and its display need to be refreshed
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);
594 * Registers a callback to be fired when the object has been updated.
596 onObjectUpdate : function(callback) {
597 entityFeedApi && entityFeedApi.callApi({method: listeners.onObjectUpdate}, callback);
601 * Reloads the frame containing this page
603 reloadFrame : function() {
604 entityFeedApi && entityFeedApi.reloadFrame();
610 sforce.interaction.initialize();