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
+ }
+}