///<reference path="../common/constants.ts" />
///<reference path="../proxy/sdkClientProxyImp.ts" />
///<reference path="../telemetry/telemetry.ts" />
///<reference path="../utils/browser.ts" />
///<reference path="../utils/guid.ts" />
///<reference path="../utils/object.ts" />
///<reference path="../utils/url.ts" />
///<reference path="uiConfig.ts" />
namespace internal.ui {
const DEFAULT_WINDOW_WIDTH = 600;
const DEFAULT_WINDOW_HEIGHT = 800;
export const RENDER_OPERATION_NAME = "ui.render";
/**
* Refer to https://www.typescriptlang.org/docs/handbook/generics.html for usage of Generics in TypeScript
* Render a UI component with Qos event enabled
* @method renderComponent
* @private
* @param {TComponent} component Generic type that extends the UIComponent class
* @param {TConfig} config Generic type that extends the UIConfig class
* @param {string} componentType A string defined by each UI component to represent its type. e.g. "Chat", "Callback", "VirtualAgent", etc ...
* @return {Promise}
*/
export function renderComponent<TComponent extends UIComponent, TConfig extends UIConfig>(
component: { new (config: TConfig): TComponent },
config: TConfig,
componentType: string): JQueryPromise<any> {
return telemetry.captureIncomingRequestAsync(
(qos: telemetry.QosEventData): JQueryPromise<any> => {
return new component(config).render();
},
RENDER_OPERATION_NAME,
componentType,
config.uiInfo);
}
export interface WindowProperties {
width: number;
height: number;
resizable: number; // 1 or 0
scrollbars: number; // 1 or 0
}
export interface ComponentSDK { }
export abstract class UIComponent {
public config: UIConfig;
public id: string;
public loadTimeoutMs: number;
public reportsHeightChanges: boolean;
public proxy: proxy.SdkClientProxy;
public iframe: HTMLIFrameElement;
public popup: Window;
protected renderDeferred: JQueryDeferred<any>;
protected renderTimeout: number;
// Abstract methods which MUST be overridden by every UI Component class
public abstract getComponentUrl(): string;
public abstract getComponentSdk(): ComponentSDK;
public abstract getXframeProxyUrl(): string;
constructor(config: UIConfig, loadTimeoutMs?: number, reportsHeightChanges?: boolean) {
if (!config || !config.uiInfo || !config.uiInfo.type) {
throw new Error("config does not contain the type of UI to be rendered");
}
this.config = utils.cloneObject({}, config);
this.id = utils.generateUniqueId();
this.loadTimeoutMs = loadTimeoutMs || 0; // 0 means renderUI will not wait for LOADED event from the UI component, and always resolves as success
this.reportsHeightChanges = !!reportsHeightChanges;
}
public render(): JQueryPromise<any> {
let cV = internal.telemetry.getCorrelationVector();
let sessionId = SdkConfig.current.sessionId;
let params: any = {};
params[`${internal.SDK_QUERY_PARAM_NAME.APP_ID}`] = SdkConfig.current.appId;
params[`${internal.SDK_QUERY_PARAM_NAME.PARTNER_ID}`] = SdkConfig.current.partnerId;
params[`${internal.SDK_QUERY_PARAM_NAME.COMPONENT_ID}`] = this.id;
params[`${internal.SDK_QUERY_PARAM_NAME.TARGET_ORIGIN}`] = utils.getCurrentOrigin();
cV && (params[`${internal.SDK_QUERY_PARAM_NAME.MS_CV}`] = cV);
sessionId && (params[`${internal.SDK_QUERY_PARAM_NAME.SESSION_ID}`] = sessionId);
// TODO cheliu: remove the following line after all service backend move to the new sdk integration library
params[`${internal.SDK_QUERY_PARAM_NAME.HOST_TYPE}`] = this.config.uiInfo.type;
let listeners = this.getEventListeners();
// add LOADED and HEIGHT_CHANGED event handlers for all ui components if supported
if (this.loadTimeoutMs > 0) {
listeners[<any>InternalEvent.LOADED] = (e: Event) => loadedHandler(this.renderDeferred, this.renderTimeout, this);
}
if (this.reportsHeightChanges) {
listeners[<any>InternalEvent.HEIGHT_CHANGED] = (e: Event) => heightChangeHandler(this.iframe, e.data);
}
// add event listeners for signInRequest
listeners[<any>CommonEvent.SIGNIN_REQUEST] = (e: Event) => {
// delegated auth request is handled internally
if (e.data && e.data.length > 0 && e.data[0].type === internal.ui.AuthType.DELEGATED) {
delegatedAuthHandler(this);
}
else if (this.config.onSignInRequest instanceof Function) {
window.setTimeout(() => this.config.onSignInRequest(e), 0);
}
};
switch (this.config.uiInfo.type) {
case HostType.IFRAME:
this.renderDeferred = renderIFrame(this, params, listeners);
break;
case HostType.POPUP:
this.renderDeferred = renderPopup(this, params, listeners);
break;
default:
return jQuery.Deferred().reject(`HostType: ${this.config.uiInfo.type} is not supported`);
}
if (this.loadTimeoutMs > 0) {
this.renderTimeout = window.setTimeout(
() => {
if (this.renderDeferred.state() === PROMISE_STATE_PENDING) {
this.renderDeferred.reject(`Load timeout expired: ${this.getComponentUrl()}`);
}
},
this.loadTimeoutMs);
}
else if (this.renderDeferred.state() === PROMISE_STATE_PENDING) {
// Component does not support loaded event
// Don't bother listening to onload event since it fires very late, and fires even on most errors
this.renderDeferred.resolve(this.getComponentSdk());
}
return this.renderDeferred.promise();
}
// Override this method in the UI component to define default window size
public getDefaultWindowProperties(): WindowProperties {
return { width: DEFAULT_WINDOW_WIDTH, height: DEFAULT_WINDOW_HEIGHT, resizable: 1, scrollbars: 1 };
}
protected getEventListeners(): Map<EventListener> {
return UIComponent.getEventListeners(this.config);
}
protected static getEventListeners(config: any, prefix?: string): Map<EventListener> {
// iterate through the config object properties and add event listener if
// the property name starts with "on" and the property is a function
let listeners: Map<EventListener> = {};
for (let prop in config) {
if (typeof prop === "string" &&
prop.length > 2 &&
prop.slice(0, 2) === "on" &&
config.hasOwnProperty(prop) &&
config[prop] instanceof Function) {
listeners[getEventName(prefix, prop.substr(2))] = config[prop];
}
}
return listeners;
}
}
// Get a DOM element on the page using a jquery selector
function getElement(containerSelector: string): HTMLElement {
let elements = jQuery(containerSelector);
return elements.length > 0 ? elements[0] : undefined;
}
// Creates an iframe element that can be attached to the DOM
function createIframeElement(src: string, id: string, params: any, style?: any, title?: string): HTMLIFrameElement {
let iframe = document.createElement("iframe");
iframe.src = utils.appendParams(src, params, true);
iframe.id = id;
iframe.style.display = (style && style.display) || "block"; // Avoids inline whitespace that causes scrollbars at 100%
iframe.style.height = (style && style.height) || "100%";
iframe.style.width = (style && style.width) || "100%";
iframe.title = title || "";
iframe.frameBorder = "0";
return iframe;
}
// get window height and width from (in order of preference):
// - the user-specified config
// - the default config
function getWindowProperties(config: UIConfig, defaultProperties: WindowProperties): string {
let width = config && config.uiInfo && config.uiInfo.width;
let height = config && config.uiInfo && config.uiInfo.height;
if (!(width >= 0 && height >= 0)) {
width = defaultProperties.width;
height = defaultProperties.height;
}
// https://developer.mozilla.org/en-US/docs/Web/API/Window/open
// windowFeatures Optional
// A DOMString containing a comma- separated list of window features given with their corresponding values in the form "name=value".
// These features include options such as the window's default size and position, whether or not to include scroll bars, and so forth.
// *** There must be no whitespace in the string. ***
let windowFeaturesString = `resizable=${defaultProperties.resizable},scrollbars=${defaultProperties.scrollbars}`;
if (width >= 0 && height >= 0) {
return `${windowFeaturesString},width=${width},height=${height}`;
}
return windowFeaturesString;
}
function heightChangeHandler(iframe: HTMLElement, height: number): void {
if (iframe && height > 0) {
iframe.style.height = `${height}px`;
}
}
function loadedHandler(deferredRender: JQueryDeferred<ComponentSDK>, timeout: number, uiComponent: UIComponent): void {
// always send the client config to the ui component after it's loaded.
uiComponent.proxy.dispatchEvent(<any>InternalEvent.CLIENT_CONFIG_UPDATE, uiComponent.config);
// only resolve the render() promise on first loaded event
if (deferredRender.state() === PROMISE_STATE_PENDING) {
try {
window.clearTimeout(timeout);
deferredRender.resolve(uiComponent.getComponentSdk());
}
catch (e) {
deferredRender.reject(`Could not create component SDK. Error: ${e}`);
}
}
else {
// TODO: Will end up here if the popup window refreshes after sending LOADED event.
}
}
function delegatedAuthHandler(uiComponent: UIComponent) {
let errorHandler = (message: string, error?: Error): void => {
console.log(message);
if (uiComponent.config.onError && (uiComponent.config.onError instanceof Function)) {
uiComponent.config.onError(
{
type: <any>CommonEvent.ERROR,
id: uiComponent.id,
data: {
message: message,
exception: error
}
});
}
};
try {
if (uiComponent.proxy && uiComponent.config.authInfo && uiComponent.config.authInfo.token) {
uiComponent.proxy.dispatchEvent(<any>CommonEvent.SIGNIN_RESPONSE, { token: uiComponent.config.authInfo.token });
}
else {
errorHandler("delegated auth token must be provided!");
}
}
catch (e) {
errorHandler(`Could not handle delegated auth request. Error: ${utils.stringify(e)}`, e);
}
}
function createProxy(targetWindow: Window, componentId: string, componentUrl: string, listeners: Map<EventListener>, onError: (e: Event) => void): proxy.SdkClientProxy {
let messageProxy = new proxy.WindowPostMessageProxy(targetWindow, utils.getOriginFromUrl(componentUrl));
return new proxy.SdkClientProxyImp(componentId, listeners, messageProxy);
}
function createXframeProxy(uiComponent: UIComponent, listeners: Map<EventListener>): proxy.SdkClientProxy {
let xframeStyle: any = {
display: "none",
height: "0",
width: "0"
};
let cV = internal.telemetry.getCorrelationVector();
let sessionId = SdkConfig.current.sessionId;
let params: any = {};
params[`${internal.SDK_QUERY_PARAM_NAME.APP_ID}`] = SdkConfig.current.appId;
params[`${internal.SDK_QUERY_PARAM_NAME.PARTNER_ID}`] = SdkConfig.current.partnerId;
params[`${internal.SDK_QUERY_PARAM_NAME.COMPONENT_ID}`] = uiComponent.id;
params[`${internal.SDK_QUERY_PARAM_NAME.TARGET_ORIGIN}`] = utils.getCurrentOrigin();
cV && (params[`${internal.SDK_QUERY_PARAM_NAME.MS_CV}`] = cV);
sessionId && (params[`${internal.SDK_QUERY_PARAM_NAME.SESSION_ID}`] = sessionId);
// TODO cheliu: remove the following line after all service backend move to the new sdk integration library
params[`${internal.SDK_QUERY_PARAM_NAME.HOST_TYPE}`] = HostType.IFRAME;
let xframe = createIframeElement(uiComponent.getXframeProxyUrl(), `${uiComponent.id}-xframeproxy`, params, xframeStyle);
getElement("body").appendChild(xframe);
// TODO: write qos for xframe proxy page
return createProxy(xframe.contentWindow, uiComponent.id, uiComponent.getComponentUrl(), listeners, uiComponent.config.onError);
}
function renderIFrame(uiComponent: UIComponent, params: any, listeners: Map<EventListener>): JQueryDeferred<any> {
let deferred = jQuery.Deferred();
let hostElement = getElement(uiComponent.config.uiInfo.containerSelector);
if (hostElement) {
uiComponent.iframe = createIframeElement(uiComponent.getComponentUrl(), uiComponent.id, params, undefined, uiComponent.config.uiInfo.accessibilityTitle);
hostElement.appendChild(uiComponent.iframe);
uiComponent.proxy = createProxy(uiComponent.iframe.contentWindow, uiComponent.id, uiComponent.getComponentUrl(), listeners, uiComponent.config.onError);
} else {
deferred.reject("Could not find hosting container: " + uiComponent.config.uiInfo.containerSelector);
}
return deferred;
}
function renderPopup(uiComponent: UIComponent, params: any, listeners: Map<EventListener>): JQueryDeferred<any> {
let deferred = jQuery.Deferred();
let windowProperties = getWindowProperties(uiComponent.config, uiComponent.getDefaultWindowProperties());
let useXframe = utils.isInternetExplorer() && !!uiComponent.getXframeProxyUrl();
params[`${internal.SDK_QUERY_PARAM_NAME.XFRAME}`] = useXframe;
let src = utils.appendParams(uiComponent.getComponentUrl(), params, true);
// IE9 only supports numbers and letters in window name
// Make sure uiComponent.id doesn't include any space or special characters.
uiComponent.popup = window.open(src, uiComponent.id, windowProperties);
uiComponent.proxy = useXframe ?
createXframeProxy(uiComponent, listeners) :
createProxy(uiComponent.popup, uiComponent.id, uiComponent.getComponentUrl(), listeners, uiComponent.config.onError);
return deferred;
}
function getEventName(prefix: string, eventName: string) {
// add prefix for non-common events
return (prefix && !CommonEvent[<any>eventName]) ? `${prefix + eventName}` : eventName;
}
}