@swrve/smarttv-sdk
Version:
Swrve marketing engagement platform SDK for SmartTV OTT devices
490 lines (419 loc) • 17.9 kB
text/typescript
import {
ISwrveButton, ISwrveCampaign, ISwrveFormat, ISwrveImage, ISwrveMessage,
} from "../ISwrveCampaign";
import * as SwrveConstants from "../../utils/SwrveConstants";
import {IPlatform} from "../../utils/platforms/IPlatform";
import {DEFAULT_IAM_STYLE, IAM_CSS_CLASS_NAME, SWRVE_IAM_CONTAINER} from "../../utils/SwrveConstants";
import SwrveFocusManager from "../../UIElements/SwrveFocusManager";
import {ISwrveInternalConfig} from "../../Config/ISwrveInternalConfig";
import {IKeyMapping} from "../../utils/platforms/IKeymapping";
import IDictionary from "../../utils/IDictionary";
import {ResourceManager} from "../../Resources/ResourceManager";
import {ICSSStyle} from "../../Config/ISwrveConfig";
import SwrveLogger from "../../utils/SwrveLogger";
import { TextTemplating } from "../../utils/TextTemplating";
export type OnBackButtonClicked = () => void;
export type OnButtonClicked = (button: ISwrveButton, parentCampaign: ISwrveCampaign, pageId: string, pageName: string) => void;
export type OnPageViewed = (messageId: number, pageId: number, pageName: string) => void;
interface IAMButton {
button: ISwrveButton;
campaign: ISwrveCampaign;
element: HTMLElement;
}
const blacklistedCSSAttributes = [
// strip out attributes used for positioning
"position", "width", "height", "top", "left",
// strip out extra attributes from resources
"uid", "name", "description", "thumbnail", "item_class",
].reduce((idx, prop) => {
idx[prop] = true;
return idx;
}, {} as IDictionary<boolean>);
const defaultStyle = {
opacity: "0.5",
transition: "opacity 150ms ease-out",
};
const defaultFocusStyle = {
opacity: "1",
transition: "opacity 150ms ease-out",
};
export class SwrveMessageDisplayManager {
private screenCenterWidth: number = 0;
private screenCenterHeight: number = 0;
private onBackButtonClickedCallback: OnBackButtonClicked | null = null;
private onButtonClickedCallback: OnButtonClicked | null = null;
private onPageViewedCallback: OnPageViewed | null = null;
private isOpen: boolean = false;
private justClosed: boolean = false;
private focusManager?: SwrveFocusManager<IAMButton>;
private resourceManager?: ResourceManager;
private normalStyle?: ICSSStyle | string;
private focusStyle?: ICSSStyle | string;
private overrideIAMStyle?: string;
private keymap: IKeyMapping;
private currentPageIndex: number = 0;
private currentMessagePages: any[] = [];
private sentPageViewEvents: number[] = [];
private sentNavigationEvents: number[] = [];
private imagesCDN: string = "";
private scale: number = 1;
private personalizationProperties?: IDictionary<string>;
constructor(platform: IPlatform, config?: ISwrveInternalConfig, resourceManager?: ResourceManager) {
this.normalStyle = config && config.inAppMessageButtonStyle;
this.focusStyle = config && config.inAppMessageButtonFocusStyle;
this.overrideIAMStyle = config && config.inAppMessageStyleOverride;
this.keymap = platform.getKeymapping();
this.resourceManager = resourceManager;
this.initListener();
}
public showMessage(
message: ISwrveMessage,
parentCampaign: ISwrveCampaign,
imagesCDN: string,
platform: IPlatform,
personalizationProperties?: IDictionary<string>,
): void {
this.currentPageIndex = 0;
this.currentMessagePages = [];
this.sentPageViewEvents = [];
this.sentNavigationEvents = [];
this.screenCenterWidth = platform.screenWidth / 2;
this.screenCenterHeight = platform.screenHeight / 2;
this.personalizationProperties = personalizationProperties;
const iam = this.getLandscapeFormat(message);
if (!iam) return;
this.isOpen = true;
this.imagesCDN = imagesCDN || '';
this.scale = iam.scale ?? 1;
this.createContainer(message.name, iam.color || undefined);
if (iam.pages && iam.pages?.length > 0) {
this.currentMessagePages = iam.pages;
this.renderPage(this.currentPageIndex, parentCampaign);
} else {
this.renderContent(iam.images, iam.buttons, parentCampaign);
}
}
public onPageViewed(callback: OnPageViewed): void {
this.onPageViewedCallback = callback;
}
public onButtonClicked(callback: OnButtonClicked): void {
this.onButtonClickedCallback = callback;
}
public onBackButtonClicked(callback: OnBackButtonClicked): void {
this.onBackButtonClickedCallback = callback;
}
public isIAMShowing(): boolean {
return this.isOpen;
}
public getSentPageViewEvents(): number[] {
return this.sentPageViewEvents;
}
public getSentNavigationEvents(): number[] {
return this.sentNavigationEvents;
}
public closeMessage(): void {
const container = document.getElementById(SWRVE_IAM_CONTAINER);
if (container) {
document.body.removeChild(container);
}
this.isOpen = false;
this.justClosed = true;
delete this.focusManager;
}
private onKeydown = (ev: KeyboardEvent) => {
if (!this.isOpen) {
return;
}
ev.preventDefault();
ev.stopImmediatePropagation();
const key = this.keymap[ev.keyCode];
if (key === "Back") {
this.closeMessage();
if (this.onBackButtonClickedCallback) {
this.onBackButtonClickedCallback();
}
} else if (key && this.focusManager) {
this.focusManager.onKeyPress(key);
}
}
private onKeyup = (ev: KeyboardEvent) => {
// Closing the message will fire up one last "keyup" event
// This prevents that keyup event from spreading down to the app
if (this.justClosed) {
ev.preventDefault();
ev.stopImmediatePropagation();
this.justClosed = false;
return;
}
}
private initListener(): void {
window.addEventListener(
"keydown",
this.onKeydown,
true,
);
window.addEventListener(
"keyup",
this.onKeyup,
true,
);
}
private getLandscapeFormat(message: ISwrveMessage): ISwrveFormat | null {
const formats = message.template.formats || null;
if (formats) {
let landscape: ISwrveFormat | null = null;
formats.forEach(format => {
if (format.orientation === "landscape") {
landscape = format;
}
});
return landscape;
} else {
return null;
}
}
private createFocusManager(buttons: ReadonlyArray<IAMButton>): SwrveFocusManager<IAMButton> {
return new SwrveFocusManager<IAMButton>(buttons, {
direction: "bidirectional",
onFocus: (btn) => this.applyElementStyle(btn.element, this.getFocusStyle(this.focusStyle, defaultFocusStyle)),
onBlur: (btn) => this.applyElementStyle(btn.element, this.getFocusStyle(this.normalStyle, defaultStyle)),
onKeyPress: ({ button, campaign }, key): boolean => {
if (key === "Enter") {
this.handleButton(button, campaign);
return true;
}
return false;
},
});
}
private applyElementStyle(el: HTMLElement, style: ICSSStyle): void {
for (const attr in style) {
if (style.hasOwnProperty(attr)) {
el.style[<any> attr] = style[attr];
}
}
}
private getFocusStyle(style: ICSSStyle | string | undefined, defaults: ICSSStyle): ICSSStyle {
let ret = defaults;
if (typeof style === "string") {
if (this.resourceManager) {
const resource = this.resourceManager.getResource(style).toJSON();
if (Object.keys(resource).length !== 0) {
ret = this.sanitizeFocusStyle(resource);
}
}
} else if (style) {
ret = this.sanitizeFocusStyle(style);
}
return ret;
}
private sanitizeFocusStyle(style: ICSSStyle): ICSSStyle {
const ret: ICSSStyle = {};
for (const key in style) {
if (style.hasOwnProperty(key) && !blacklistedCSSAttributes[key]) {
ret[key] = style[key];
}
}
return ret;
}
private appendImages(images: ReadonlyArray<ISwrveImage>): void {
images.forEach((image, index) => {
const imageElement = document.createElement("img");
imageElement.id = "SwrveImage" + index;
if (image.dynamic_image_url && image.dynamic_image_url.length > 0) {
if (this.personalizationProperties) {
imageElement.src = this.personalizeText(image.dynamic_image_url, this.personalizationProperties) ?? "";
} else {
imageElement.src = image.dynamic_image_url;
}
} else {
imageElement.src = this.imagesCDN + image.image.value as string;
}
this.addElement(image, imageElement);
});
}
private appendButtons(
buttons: ReadonlyArray<ISwrveButton>, parentCampaign: ISwrveCampaign): IAMButton[] {
return buttons.map((button, index) => {
const buttonElement = document.createElement("img");
const buttonStyle = this.getFocusStyle(this.normalStyle, defaultStyle);
const personalizedButton = this.personalizeButton(button);
buttonElement.id = "SwrveButton" + index;
if (personalizedButton.dynamic_image_url && personalizedButton.dynamic_image_url.length > 0) {
buttonElement.src = personalizedButton.dynamic_image_url;
} else {
buttonElement.src = this.imagesCDN + personalizedButton.image_up.value as string;
}
buttonElement.style.border = "0px";
buttonElement.onclick = () => this.handleButton(personalizedButton, parentCampaign);
this.applyElementStyle(buttonElement, buttonStyle);
this.addElement(button, buttonElement);
return {
button,
campaign: parentCampaign,
element: buttonElement,
};
});
}
private addElement(swrveItem: ISwrveButton | ISwrveImage, element: HTMLImageElement): void {
if (typeof swrveItem.x.value === "number" && typeof swrveItem.y.value === "number") {
const container = document.getElementById(SWRVE_IAM_CONTAINER);
let width = element.naturalWidth * this.scale;
let height = element.naturalHeight * this.scale;
if (swrveItem.dynamic_image_url) {
width = swrveItem.w.value as number * this.scale;
height = swrveItem.h.value as number * this.scale;
//Aspect fit
const aspectRatio = element.naturalWidth / element.naturalHeight;
if (width / height > aspectRatio) {
width = height * aspectRatio;
} else {
height = width / aspectRatio;
}
}
const yPos = swrveItem.y.value;
const xPos = swrveItem.x.value;
if (width > height) {
element.style.width = width.toString() + "px";
} else {
element.style.height = height.toString() + "px";
}
element.style.position = "absolute";
element.style.top = (yPos + (this.screenCenterHeight - (height / 2))).toString() + "px";
element.style.left = (xPos + (this.screenCenterWidth - (width / 2))).toString() + "px";
if (swrveItem.accessibility_text) {
if (this.personalizationProperties) {
const personalizedAltText = this.personalizeText(swrveItem.accessibility_text, this.personalizationProperties);
if (personalizedAltText) {
element.alt = personalizedAltText;
}
} else {
element.alt = swrveItem.accessibility_text;
}
}
container!.appendChild(element);
}
}
private handleButton(button: ISwrveButton, parentCampaign: ISwrveCampaign): void {
const type = String(button.type.value);
const action = String(button.action.value);
const pageId = this.getCurrentPageId();
const pageName = this.getCurrentPageName();
if (type === SwrveConstants.PAGE_LINK) {
const num: number = Number(action);
this.changePage(num, parentCampaign);
} else {
this.closeMessage();
}
if (this.onButtonClickedCallback) {
this.onButtonClickedCallback(button, parentCampaign, pageId, pageName);
}
}
private createContainer(name: string, color?: string): void {
const iamContainer = document.createElement("div");
iamContainer.id = SWRVE_IAM_CONTAINER;
iamContainer.className = IAM_CSS_CLASS_NAME;
iamContainer.innerHTML = this.overrideIAMStyle || DEFAULT_IAM_STYLE;
iamContainer.style.backgroundColor = color || "";
document.body.appendChild(iamContainer);
}
private changePage(newPageId: number, parentCampaign: ISwrveCampaign): void {
const newPageIndex = this.currentMessagePages.findIndex(page => page.page_id === newPageId);
if (newPageIndex !== -1) {
this.currentPageIndex = newPageIndex;
this.renderPage(newPageIndex, parentCampaign);
}
}
private renderPage(
pageIndex: number,
parentCampaign: ISwrveCampaign,
): void {
const container = document.getElementById(SWRVE_IAM_CONTAINER);
if (!container) return;
// Cache and reapply style content
const styleContent = container.querySelector("style")?.outerHTML || "";
container.innerHTML = styleContent;
const page = this.currentMessagePages[pageIndex];
if (page) {
this.renderContent(page.images, page.buttons, parentCampaign);
}
}
private renderContent(
images: ReadonlyArray<ISwrveImage> | undefined,
buttons: ReadonlyArray<ISwrveButton> | undefined,
parentCampaign: ISwrveCampaign,
): void {
if (images && images.length > 0) {
this.appendImages(images);
}
if (buttons && buttons.length > 0) {
const buttonElements = this.appendButtons(buttons, parentCampaign);
this.focusManager = this.createFocusManager(buttonElements);
this.focusManager.setActiveFirst();
}
if (this.onPageViewedCallback) {
const messageId = parentCampaign.messages && parentCampaign.messages[0] ? parentCampaign.messages[0].id : 0;
const page = this.currentMessagePages[this.currentPageIndex];
if (page) {
this.onPageViewedCallback(messageId, page.page_id, page.page_name);
} else {
this.onPageViewedCallback(messageId, 0, "");
}
}
}
private getCurrentPageId(): string {
if (!this.currentMessagePages || this.currentMessagePages.length === 0) {
return "";
}
if (this.currentPageIndex < 0 || this.currentPageIndex >= this.currentMessagePages.length) {
return "";
}
const page = this.currentMessagePages[this.currentPageIndex];
return page.page_id.toString();
}
private getCurrentPageName(): string {
if (!this.currentMessagePages || this.currentMessagePages.length === 0) {
return "";
}
if (this.currentPageIndex < 0 || this.currentPageIndex >= this.currentMessagePages.length) {
return "";
}
const page = this.currentMessagePages[this.currentPageIndex];
return page.page_name;
}
private personalizeButton(button: ISwrveButton): ISwrveButton {
if (this.personalizationProperties == null) {
return button;
}
let personalizedButton = button;
if (button.type.value === SwrveConstants.CUSTOM
&& button.action?.value
&& typeof button.action.value === 'string') {
const personalizedAction = this.personalizeText(button.action.value, this.personalizationProperties);
if (personalizedAction) {
personalizedButton = JSON.parse(JSON.stringify(button)) as typeof button;
personalizedButton.action.value = personalizedAction;
}
}
if (button.dynamic_image_url) {
const personalizedUrl = this.personalizeText(button.dynamic_image_url, this.personalizationProperties);
if (personalizedUrl) {
if (!personalizedButton) {
personalizedButton = JSON.parse(JSON.stringify(button)) as typeof button;
}
personalizedButton.dynamic_image_url = personalizedUrl;
}
}
return personalizedButton;
}
private personalizeText(text: string, personalizationProperties: IDictionary<string>): string | null {
if (text != null) {
try {
return TextTemplating.applyTextTemplatingToString(text, personalizationProperties);
} catch (e) {
SwrveLogger.error("Could not resolve, error with personalization", e);
}
}
return null;
}
}