UNPKG

@swrve/smarttv-sdk

Version:

Swrve marketing engagement platform SDK for SmartTV OTT devices

490 lines (419 loc) 17.9 kB
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; } }