diff --git a/src/util/campaignHouseKeeping/commonCampaignUtils.js b/src/util/campaignHouseKeeping/commonCampaignUtils.js index d7440b86..059c99a9 100644 --- a/src/util/campaignHouseKeeping/commonCampaignUtils.js +++ b/src/util/campaignHouseKeeping/commonCampaignUtils.js @@ -1,4 +1,4 @@ -import { renderAdvancedBuilder, renderPopUpImageOnly } from '../campaignRender/webPopup.js' +import { renderAdvancedBuilder, renderPopUpImageOnly, renderPIP } from '../campaignRender/webPopup.js' import { addDeliveryPreferenceDetails, addToLocalProfileMap, @@ -26,6 +26,7 @@ import { getNow, getToday } from '../datetime.js' import { StorageManager, $ct } from '../storage.js' import RequestDispatcher from '../requestDispatcher.js' import { CTWebPopupImageOnly } from '../web-popupImageonly/popupImageonly.js' +import { CTWebPopupPIP } from '../web-popupPIP/popupPIP.js' import { checkAndRegisterWebInboxElements, initializeWebInbox, @@ -435,6 +436,28 @@ export const commonCampaignUtils = { return renderPopUpImageOnly(targetingMsgJson, CampaignContext.session) }, + handlePIP (targetingMsgJson) { + const divId = 'wizPIPDiv' + // Skips if frequency limits are exceeded + if (this.doCampHouseKeeping(targetingMsgJson, Logger.getInstance()) === false) { + return + } + // Removes existing popup if spam control is active + if ($ct.dismissSpamControl && document.getElementById(divId) != null) { + const element = document.getElementById(divId) + element.remove() + } + const msgDiv = document.createElement('div') + msgDiv.id = divId + document.body.appendChild(msgDiv) + // Registers custom element for PIP if not already defined + if (customElements.get('ct-web-popup-pip') === undefined) { + customElements.define('ct-web-popup-pip', CTWebPopupPIP) + } + // Renders the PIP + return renderPIP(targetingMsgJson, CampaignContext.session) + }, + // Checks if a campaign is already rendered in an iframe isExistingCampaign (campaignId) { const testIframe = @@ -463,6 +486,10 @@ export const commonCampaignUtils = { this.handleImageOnlyPopup(targetingMsgJson) return } + if (displayObj.layout === WEB_POPUP_TEMPLATES.PIP) { + this.handlePIP(targetingMsgJson) + return + } // Skips if frequency limits are exceeded if (this.doCampHouseKeeping(targetingMsgJson, Logger.getInstance()) === false) { diff --git a/src/util/campaignRender/webPopup.js b/src/util/campaignRender/webPopup.js index f8a9efb3..c2bdd066 100644 --- a/src/util/campaignRender/webPopup.js +++ b/src/util/campaignRender/webPopup.js @@ -16,6 +16,17 @@ export const renderPopUpImageOnly = (targetingMsgJson, _session) => { containerEl.appendChild(popupImageOnly) } +export const renderPIP = (targetingMsgJson, _session) => { + const divId = 'wizPIPDiv' + const pip = document.createElement('ct-web-popup-pip') + pip.session = _session + pip.target = targetingMsgJson + const containerEl = document.getElementById(divId) + containerEl.innerHTML = '' + containerEl.style.visibility = 'hidden' + containerEl.appendChild(pip) +} + const FULLSCREEN_STYLE = ` z-index: 2147483647; display: block; diff --git a/src/util/constants.js b/src/util/constants.js index c7e2f8fd..625abb39 100644 --- a/src/util/constants.js +++ b/src/util/constants.js @@ -104,9 +104,22 @@ export const WEB_POPUP_TEMPLATES = { INTERSTITIAL: 1, BANNER: 2, IMAGE_ONLY: 3, - ADVANCED_BUILDER: 4 + ADVANCED_BUILDER: 4, + PIP: 5 } +/** Inner HTML for `#ct-pip-play svg` when video is playing (`msgContent.html` only ships play). Path uses viewBox 0 0 48 48. */ +export const PIP_PAUSE_ICON_SVG = + '' + +/** Inner HTML for `#ct-pip-expand svg` when PIP is fullscreen (collapse control). Path uses viewBox 0 0 48 48. */ +export const PIP_COLLAPSE_ICON_SVG = + '' + +/** Inner HTML for `#ct-pip-mute svg` when video is muted (unmute action). Path uses viewBox 0 0 48 48; mute glyph comes from `msgContent.html`. */ +export const PIP_UNMUTE_ICON_SVG = + '' + export const CAMPAIGN_TYPES = { EXIT_INTENT: 1, /* Deprecated */ WEB_NATIVE_DISPLAY: 2, diff --git a/src/util/web-popupPIP/pipPopupUtils.js b/src/util/web-popupPIP/pipPopupUtils.js new file mode 100644 index 00000000..f49bda8e --- /dev/null +++ b/src/util/web-popupPIP/pipPopupUtils.js @@ -0,0 +1,181 @@ +import { ACTION_TYPES } from '../constants' +import { invokeExternalJs } from '../campaignRender/utilities' + +export const PIP_DRAG_CONTROL_SELECTOR = + '#ct-pip-close, #ct-pip-expand, #ct-pip-play, #ct-pip-mute' + +/** Fullscreen expand: letterbox video with native aspect ratio (object-fit: contain). */ +export const PIP_EXPAND_RUNTIME_CSS = ` +.ct-pip-overlay.ct-pip--expanded { + align-items: center !important; + justify-content: center !important; + padding: 0 !important; + pointer-events: auto !important; + background: rgba(0, 0, 0, 0.88); +} +.ct-pip-overlay.ct-pip--expanded .ct-pip-container { + position: relative !important; + max-width: none !important; + width: 100vw !important; + height: 100vh !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + border-radius: 0 !important; +} +.ct-pip-overlay.ct-pip--expanded .ct-pip-media { + width: auto !important; + height: auto !important; + max-width: 100vw !important; + max-height: 100vh !important; + object-fit: contain !important; +} +` + +/** Nearest anchor id -> flex placement on `.ct-pip-overlay` (row: justify = horizontal, align = vertical). */ +export const PIP_ANCHOR_FLEX = { + center: { alignItems: 'center', justifyContent: 'center' }, + 'top-right': { alignItems: 'flex-start', justifyContent: 'flex-end' }, + 'top-left': { alignItems: 'flex-start', justifyContent: 'flex-start' }, + 'bottom-right': { alignItems: 'flex-end', justifyContent: 'flex-end' }, + 'bottom-left': { alignItems: 'flex-end', justifyContent: 'flex-start' }, + top: { alignItems: 'flex-start', justifyContent: 'center' }, + bottom: { alignItems: 'flex-end', justifyContent: 'center' }, + left: { alignItems: 'center', justifyContent: 'flex-start' }, + right: { alignItems: 'center', justifyContent: 'flex-end' } +} + +/** + * `display.pip.onClick` / `display.mobile.pip.onClick` (nested payload). + * @param {Record} pipConfig — result of desktop/mobile `pip` slice + * @returns {Record} + */ +export function getPipOnClickConfig (pipConfig) { + const oc = pipConfig?.onClick + return oc && typeof oc === 'object' ? oc : {} +} + +/** @param {Record} pipConfig */ +export function getPipOnClickAction (pipConfig) { + const t = getPipOnClickConfig(pipConfig).type + if (t != null) { + const s = String(t).trim() + if (s !== '') return s + } + const legacy = pipConfig?.onClickAction + return legacy != null && legacy !== '' ? legacy : '' +} + +/** @param {Record} pipConfig */ +export function getPipOnClickUrl (pipConfig, display) { + const oc = getPipOnClickConfig(pipConfig) + return ( + oc.webUrl || + oc.url || + pipConfig?.onClickUrl || + display?.onClickUrl || + '' + ) +} + +/** Used only for {@link ACTION_TYPES.OPEN_LINK} (`url`). */ +export function getPipOpenLinkUsesNewTab (pipConfig, display) { + if (typeof pipConfig?.window === 'boolean') return pipConfig.window + return !!display?.window +} + +/** @param {Record} pipConfig */ +export function getPipOnClickJsName (pipConfig) { + const oc = getPipOnClickConfig(pipConfig) + if (typeof oc.js === 'string' && oc.js) return oc.js + if (typeof oc.jsName === 'string' && oc.jsName) return oc.jsName + if (oc.js && typeof oc.js === 'object' && oc.js?.name) return oc.js.name + return pipConfig?.onClickJs || '' +} + +export function buildPipNotificationClickedPayload (msgId, pivotId, pipConfig) { + const oc = getPipOnClickConfig(pipConfig) + const kv = oc.kv + const payload = { msgId, pivotId } + if (kv && typeof kv === 'object' && Object.keys(kv).length > 0) { + payload.kv = kv + } + return payload +} + +function navigateOpenWebUrl (url, oc, closeTemplate) { + const openInNewTab = oc.openInNewTab === true + const closeOnClick = oc.closeOnClick === true + if (openInNewTab) { + window.open(url, '_blank', 'noopener') + if (closeOnClick) closeTemplate() + } else { + if (closeOnClick) closeTemplate() + window.location.href = url + } +} + +/** + * @param {object} params + * @param {Record} params.pipConfig — `getPipDisplayConfig()` slice + * @param {Record} params.display — `target.display` + * @param {Record} params.targetingMsgJson — campaign / `target` + * @param {boolean} [params.preview] + * @param {() => void} params.closeTemplate + */ +export function runPipClickAction ({ + pipConfig, + display, + targetingMsgJson, + preview, + closeTemplate +}) { + const action = getPipOnClickAction(pipConfig) + if (!action) return + + const fireClicked = () => { + if (!preview) { + window.clevertap.renderNotificationClicked( + buildPipNotificationClickedPayload( + targetingMsgJson.wzrk_id, + targetingMsgJson.wzrk_pivot, + pipConfig + ) + ) + } + } + + switch (action) { + case ACTION_TYPES.OPEN_LINK: { + const url = getPipOnClickUrl(pipConfig, display) + if (!url) return + fireClicked() + if (getPipOpenLinkUsesNewTab(pipConfig, display)) { + window.open(url, '_blank', 'noopener') + } else { + window.parent.location.href = url + } + break + } + case ACTION_TYPES.OPEN_WEB_URL: { + const url = getPipOnClickUrl(pipConfig, display) + if (!url) return + fireClicked() + navigateOpenWebUrl(url, getPipOnClickConfig(pipConfig), closeTemplate) + break + } + case ACTION_TYPES.SOFT_PROMPT: + fireClicked() + window.clevertap.notifications.push({ skipDialog: true }) + break + case ACTION_TYPES.RUN_JS: { + const jsName = getPipOnClickJsName(pipConfig) + if (!jsName) return + fireClicked() + invokeExternalJs(jsName, targetingMsgJson) + break + } + default: + break + } +} diff --git a/src/util/web-popupPIP/popupPIP.js b/src/util/web-popupPIP/popupPIP.js new file mode 100644 index 00000000..70b59e76 --- /dev/null +++ b/src/util/web-popupPIP/popupPIP.js @@ -0,0 +1,587 @@ +import { + getCampaignObject, + saveCampaignObject +} from '../clevertap' +import { + PIP_COLLAPSE_ICON_SVG, + PIP_PAUSE_ICON_SVG, + PIP_UNMUTE_ICON_SVG +} from '../constants' +import { StorageManager } from '../storage' +import { + PIP_ANCHOR_FLEX, + PIP_DRAG_CONTROL_SELECTOR, + PIP_EXPAND_RUNTIME_CSS, + getPipOnClickAction, + runPipClickAction as executePipClickAction +} from './pipPopupUtils' + +export class CTWebPopupPIP extends HTMLElement { + constructor () { + super() + this.shadow = this.attachShadow({ mode: 'open' }) + } + + getShadowRoot () { + return this.shadow + } + + _target = null + _session = null + shadow = null + popup = null + container = null + mediaDesktop = null + mediaMobile = null + resizeObserver = null + _revealFallbackTimer = undefined + _pipDragAbort = null + /** @type {HTMLElement | null} */ + pipOverlay = null + _pipDragPointerId = null + playControlBtn = null + expandControlBtn = null + muteControlBtn = null + _pipExpanded = false + _pipPlaySvgFromTemplate = '' + _pipExpandSvgFromTemplate = '' + _pipMuteSvgFromTemplate = '' + + get target () { + return this._target || '' + } + + set target (val) { + if (this._target === null) { + this._target = val + this.renderPIPPopup() + } + } + + get session () { + return this._session || '' + } + + set session (val) { + this._session = val + } + + get msgId () { + return this.target.wzrk_id + } + + get pivotId () { + return this.target.wzrk_pivot + } + + get desktopAltText () { + return this.target.display.desktopAlt || this.target.display?.media?.alt_text + } + + get mobileAltText () { + return this.target.display.mobileALt || + this.target.display?.mobile?.media?.alt_text || + this.target.display?.media?.alt_text + } + + /** Desktop vs mobile PIP config and media use a 480px breakpoint. */ + isWideViewport () { + return window.innerWidth > 480 + } + + getActiveMedia () { + return this.isWideViewport() ? this.mediaDesktop : this.mediaMobile + } + + getPipDisplayConfig () { + const d = this.target.display + return this.isWideViewport() ? (d.pip || {}) : (d.mobile?.pip || d.pip || {}) + } + + /** @param {(v: HTMLVideoElement) => void} fn */ + forEachVideo (fn) { + if (this.mediaDesktop) fn(this.mediaDesktop) + if (this.mediaMobile) fn(this.mediaMobile) + } + + applyPopupTitle (popup) { + if (!popup) return + const title = this.isWideViewport() + ? (this.desktopAltText || '') + : (this.mobileAltText || '') + popup.setAttribute('title', title) + } + + getPipFallbackWidthPx () { + const pip = this.getPipDisplayConfig() + const pct = typeof pip.width === 'number' ? pip.width : 40 + return (window.innerWidth * pct) / 100 + } + + isPipDragEnabled () { + return this.getPipDisplayConfig().controls?.drag === true + } + + isPipPlayPauseEnabled () { + return this.getPipDisplayConfig().controls?.playPause !== false + } + + isPipExpandCollapseEnabled () { + return this.getPipDisplayConfig().controls?.expandCollapse !== false + } + + isPipMuteEnabled () { + return this.getPipDisplayConfig().controls?.mute !== false + } + + closePipTemplate () { + if (!this.container) return + const campaignId = this.target.wzrk_id.split('_')[0] + if (this.resizeObserver) { + this.resizeObserver.unobserve(this.container) + } + if (this._revealFallbackTimer !== undefined) { + clearTimeout(this._revealFallbackTimer) + this._revealFallbackTimer = undefined + } + if (this._pipDragAbort) { + this._pipDragAbort.abort() + this._pipDragAbort = null + } + this._pipDragPointerId = null + const host = document.getElementById('wizPIPDiv') + if (host) { + host.remove() + } + if (campaignId != null && campaignId !== '-1') { + if (StorageManager._isLocalStorageSupported()) { + const campaignObj = getCampaignObject() + + campaignObj.dnd = [...new Set([ + ...(campaignObj.dnd ?? []), + campaignId + ])] + saveCampaignObject(campaignObj) + } + } + } + + runPipClickAction () { + executePipClickAction({ + pipConfig: this.getPipDisplayConfig(), + display: this.target.display, + targetingMsgJson: this.target, + preview: this.target.display.preview, + closeTemplate: () => this.closePipTemplate() + }) + } + + setupPipClickAction () { + const action = getPipOnClickAction(this.getPipDisplayConfig()) + if (!action || !this.container) return + + this.container.addEventListener('click', (e) => { + if (e.target.closest(PIP_DRAG_CONTROL_SELECTOR)) return + this.runPipClickAction() + }) + } + + capturePipTemplateControlSvgs () { + const pairs = [ + [this.playControlBtn, '_pipPlaySvgFromTemplate'], + [this.expandControlBtn, '_pipExpandSvgFromTemplate'], + [this.muteControlBtn, '_pipMuteSvgFromTemplate'] + ] + for (let i = 0; i < pairs.length; i++) { + const el = pairs[i][0] + const prop = pairs[i][1] + const svg = el?.querySelector('svg') + if (svg) this[prop] = svg.innerHTML + } + } + + updatePipExpandButtonIcon () { + if (!this.expandControlBtn) return + const svg = this.expandControlBtn.querySelector('svg') + if (!svg) return + svg.innerHTML = this._pipExpanded + ? PIP_COLLAPSE_ICON_SVG + : (this._pipExpandSvgFromTemplate || '') + this.expandControlBtn.setAttribute('aria-label', this._pipExpanded ? 'Collapse' : 'Expand') + this.expandControlBtn.setAttribute('aria-expanded', this._pipExpanded ? 'true' : 'false') + } + + setPipExpanded (expanded) { + const overlay = this.pipOverlay || this.shadowRoot?.querySelector('.ct-pip-overlay') + if (!overlay || !this.container) return + this._pipExpanded = expanded + if (expanded) { + overlay.classList.add('ct-pip--expanded') + this.container.style.width = '' + this.container.style.height = '' + this.forEachVideo((v) => { + v.style.width = '' + v.style.height = '' + v.style.setProperty('object-fit', 'contain') + }) + } else { + overlay.classList.remove('ct-pip--expanded') + this.forEachVideo((v) => { + v.style.removeProperty('object-fit') + v.style.width = '' + v.style.height = '' + }) + this.handleResize(this.getActiveMedia(), this.container) + } + this.updatePipExpandButtonIcon() + } + + syncPipPlayButtonIcon () { + if (!this.isPipPlayPauseEnabled()) return + const video = this.getActiveMedia() + if (video && this.playControlBtn) { + this.updatePipPlayButtonIcon(video) + } + } + + updatePipPlayButtonIcon (video) { + if (!this.playControlBtn) return + const svg = this.playControlBtn.querySelector('svg') + if (!svg) return + const paused = video.paused + svg.innerHTML = paused + ? (this._pipPlaySvgFromTemplate || '') + : PIP_PAUSE_ICON_SVG + this.playControlBtn.setAttribute('aria-label', paused ? 'Play' : 'Pause') + } + + setupPipPlayPauseToggle () { + if (!this.isPipPlayPauseEnabled() || !this.playControlBtn) return + + const onPlayOrPause = () => this.syncPipPlayButtonIcon() + this.forEachVideo((video) => { + video.addEventListener('play', onPlayOrPause) + video.addEventListener('pause', onPlayOrPause) + }) + + this.playControlBtn.addEventListener('click', (e) => { + e.stopPropagation() + const video = this.getActiveMedia() + if (!video) return + if (video.paused) { + video.play().catch(() => {}) + } else { + video.pause() + } + }) + + this.syncPipPlayButtonIcon() + } + + syncPipMuteButtonIcon () { + if (!this.isPipMuteEnabled()) return + const video = this.getActiveMedia() + if (video && this.muteControlBtn) { + this.updatePipMuteButtonIcon(video) + } + } + + updatePipMuteButtonIcon (video) { + if (!this.muteControlBtn) return + const svg = this.muteControlBtn.querySelector('svg') + if (!svg) return + svg.innerHTML = video.muted + ? PIP_UNMUTE_ICON_SVG + : (this._pipMuteSvgFromTemplate || '') + this.muteControlBtn.setAttribute('aria-label', video.muted ? 'Unmute' : 'Mute') + this.muteControlBtn.setAttribute('aria-pressed', video.muted ? 'true' : 'false') + } + + applyPipMutedToAllVideos (muted) { + this.forEachVideo((v) => { + v.muted = muted + }) + } + + setupPipMuteToggle () { + if (!this.isPipMuteEnabled() || !this.muteControlBtn) return + + const onVolumeChange = () => this.syncPipMuteButtonIcon() + this.forEachVideo((video) => { + video.addEventListener('volumechange', onVolumeChange) + }) + + this.muteControlBtn.addEventListener('click', (e) => { + e.stopPropagation() + const video = this.getActiveMedia() + if (!video) return + this.applyPipMutedToAllVideos(!video.muted) + this.syncPipMuteButtonIcon() + }) + + this.syncPipMuteButtonIcon() + } + + setupPipExpandCollapse () { + if (!this.isPipExpandCollapseEnabled() || !this.expandControlBtn || !this.pipOverlay) { + return + } + this.expandControlBtn.addEventListener('click', (e) => { + e.stopPropagation() + this.setPipExpanded(!this._pipExpanded) + }) + this.updatePipExpandButtonIcon() + } + + snapPipContainerToAnchor () { + if (this._pipExpanded) return + const overlay = this.pipOverlay || this.shadowRoot?.querySelector('.ct-pip-overlay') + if (!overlay || !this.container) return + + const rect = this.container.getBoundingClientRect() + const cx = rect.left + rect.width / 2 + const cy = rect.top + rect.height / 2 + const iw = window.innerWidth + const ih = window.innerHeight + const pip = this.getPipDisplayConfig() + const mv = pip.margins?.vertical ?? 5 + const mh = pip.margins?.horizontal ?? 5 + const marginPxV = (ih * mv) / 100 + const marginPxH = (iw * mh) / 100 + const w = rect.width + const h = rect.height + + const anchors = [ + { key: 'center', x: iw / 2, y: ih / 2 }, + { key: 'top-right', x: iw - marginPxH - w / 2, y: marginPxV + h / 2 }, + { key: 'top-left', x: marginPxH + w / 2, y: marginPxV + h / 2 }, + { key: 'bottom-right', x: iw - marginPxH - w / 2, y: ih - marginPxV - h / 2 }, + { key: 'bottom-left', x: marginPxH + w / 2, y: ih - marginPxV - h / 2 }, + { key: 'top', x: iw / 2, y: marginPxV + h / 2 }, + { key: 'bottom', x: iw / 2, y: ih - marginPxV - h / 2 }, + { key: 'left', x: marginPxH + w / 2, y: ih / 2 }, + { key: 'right', x: iw - marginPxH - w / 2, y: ih / 2 } + ] + + const bestKey = anchors.reduce( + (acc, a) => { + const dx = cx - a.x + const dy = cy - a.y + const d = dx * dx + dy * dy + return d < acc.dist ? { key: a.key, dist: d } : acc + }, + { key: 'center', dist: Infinity } + ).key + + const flex = PIP_ANCHOR_FLEX[bestKey] + overlay.style.setProperty('align-items', flex.alignItems) + overlay.style.setProperty('justify-content', flex.justifyContent) + + this.container.style.position = '' + this.container.style.left = '' + this.container.style.top = '' + this.container.style.right = '' + this.container.style.bottom = '' + this.container.style.margin = '' + } + + setupPipDrag () { + if (!this.isPipDragEnabled() || !this.container) return + this.pipOverlay = this.shadowRoot.querySelector('.ct-pip-overlay') + if (!this.pipOverlay) return + + this.container.style.touchAction = 'none' + if (this._pipDragAbort) { + this._pipDragAbort.abort() + } + this._pipDragAbort = new AbortController() + const { signal } = this._pipDragAbort + + let dragOffsetX = 0 + let dragOffsetY = 0 + + const onPointerDown = (e) => { + if (this._pipExpanded) return + if (e.pointerType === 'mouse' && e.button !== 0) return + if (e.target.closest(PIP_DRAG_CONTROL_SELECTOR)) return + + this._pipDragPointerId = e.pointerId + const r = this.container.getBoundingClientRect() + dragOffsetX = e.clientX - r.left + dragOffsetY = e.clientY - r.top + try { + this.container.setPointerCapture(e.pointerId) + } catch (_err) { + this._pipDragPointerId = null + return + } + + this.container.style.position = 'fixed' + this.container.style.left = `${r.left}px` + this.container.style.top = `${r.top}px` + this.container.style.right = 'auto' + this.container.style.bottom = 'auto' + this.container.style.margin = '0' + } + + const onPointerMove = (e) => { + if (this._pipDragPointerId == null || e.pointerId !== this._pipDragPointerId) { + return + } + let left = e.clientX - dragOffsetX + let top = e.clientY - dragOffsetY + const cw = this.container.offsetWidth + const ch = this.container.offsetHeight + left = Math.max(0, Math.min(left, window.innerWidth - cw)) + top = Math.max(0, Math.min(top, window.innerHeight - ch)) + this.container.style.left = `${left}px` + this.container.style.top = `${top}px` + } + + const endDrag = (e) => { + if (this._pipDragPointerId == null || e.pointerId !== this._pipDragPointerId) { + return + } + try { + this.container.releasePointerCapture(e.pointerId) + } catch (_err) {} + this._pipDragPointerId = null + this.snapPipContainerToAnchor() + } + + this.container.addEventListener('pointerdown', onPointerDown, { signal }) + this.container.addEventListener('pointermove', onPointerMove, { signal }) + this.container.addEventListener('pointerup', endDrag, { signal }) + this.container.addEventListener('pointercancel', endDrag, { signal }) + } + + renderPIPPopup () { + this._pipExpanded = false + this.shadow.innerHTML = this.getPIPPopupContent() + const expandStyle = document.createElement('style') + expandStyle.textContent = PIP_EXPAND_RUNTIME_CSS + this.shadow.appendChild(expandStyle) + this.mediaDesktop = this.shadowRoot.querySelector('.ct-pip-media--desktop') + this.mediaMobile = this.shadowRoot.querySelector('.ct-pip-media--mobile') + this.popup = this.getActiveMedia() + this.container = this.shadowRoot.getElementById('ct-pip-container') + this.closeIcon = this.shadowRoot.getElementById('ct-pip-close') + this.playControlBtn = this.shadowRoot.getElementById('ct-pip-play') + this.expandControlBtn = this.shadowRoot.getElementById('ct-pip-expand') + this.muteControlBtn = this.shadowRoot.getElementById('ct-pip-mute') + this.pipOverlay = this.shadowRoot.querySelector('.ct-pip-overlay') + + if (!this.container) { + return + } + + this.container.setAttribute('role', 'dialog') + this.container.setAttribute('aria-modal', 'true') + + this.capturePipTemplateControlSvgs() + + const tryReveal = () => this.revealPIPContent(false) + const forceReveal = () => this.revealPIPContent(true) + this.forEachVideo((video) => { + video.addEventListener('loadeddata', tryReveal, { once: true }) + video.addEventListener('error', forceReveal, { once: true }) + }) + tryReveal() + + this._revealFallbackTimer = window.setTimeout(forceReveal, 2500) + + this.resizeObserver = new ResizeObserver(() => + this.handleResize(this.getActiveMedia(), this.container)) + this.resizeObserver.observe(this.container) + + if (this.closeIcon) { + this.closeIcon.addEventListener('click', () => this.closePipTemplate()) + } + + if (!this.target.display.preview) { + window.clevertap.renderNotificationViewed({ + msgId: this.msgId, + pivotId: this.pivotId + }) + } + + this.setupPipPlayPauseToggle() + this.setupPipMuteToggle() + this.setupPipExpandCollapse() + this.setupPipDrag() + this.setupPipClickAction() + } + + handleResize (popup, container) { + this.popup = popup + if (this._pipExpanded) { + if (!popup) return + this.applyPopupTitle(popup) + this.syncPipPlayButtonIcon() + this.syncPipMuteButtonIcon() + return + } + let width = this.getRenderedVideoWidth(popup) + if (popup && container && width <= 0) { + width = this.getPipFallbackWidthPx() + } + if (popup && container && width > 0) { + container.style.setProperty('width', `${width}px`) + } + if (!popup) return + this.applyPopupTitle(popup) + this.syncPipPlayButtonIcon() + this.syncPipMuteButtonIcon() + } + + getPIPPopupContent () { + return ` + ${this.target.msgContent.css} + ${this.target.msgContent.html} + ` + } + + revealPIPContent (force) { + if (!this.container) return + this.popup = this.getActiveMedia() + const hasRenderableMedia = !!(this.mediaDesktop || this.mediaMobile) + const activeReady = this.popup && + this.popup.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA + if (hasRenderableMedia && !activeReady && !force) { + return + } + if (this.popup && hasRenderableMedia) { + let width = this.getRenderedVideoWidth(this.popup) + if (width <= 0) { + width = this.getPipFallbackWidthPx() + } + if (width > 0) { + this.popup.style.setProperty('width', `${width}px`) + this.container.style.setProperty('width', `${width}px`) + } + } + this.container.style.setProperty('height', 'auto') + if (this.popup) { + this.popup.style.setProperty('visibility', 'visible') + } + if (this.closeIcon) { + this.closeIcon.style.setProperty('visibility', 'visible') + } + const host = document.getElementById('wizPIPDiv') + if (host) { + host.style.visibility = 'visible' + } + } + + getRenderedVideoWidth (video) { + if (!video || !video.videoWidth || !video.videoHeight) { + return 0 + } + const ratio = video.videoWidth / video.videoHeight + const h = video.clientHeight || video.offsetHeight + if (!h || h < 1) { + return this.getPipFallbackWidthPx() + } + return h * ratio + } +}