Skip to content

Commit f9da550

Browse files
perholmangseenthis-alex
authored andcommitted
SeenThis Brand Stories Rendering Module: initial release (prebid#13834)
* Add SeenThis Brand Stories module * test: add unit tests for seenthisBrandStories module functions and constants * remove support for loading inside iframe * only allow events of seenthis origin --------- Co-authored-by: Alexander Öström <[email protected]>
1 parent 4fc064a commit f9da550

File tree

3 files changed

+487
-0
lines changed

3 files changed

+487
-0
lines changed

modules/seenthisBrandStories.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Overview
2+
3+
Module Name: SeenThis Brand Stories
4+
Maintainer: [email protected]
5+
6+
# Description
7+
8+
Module to allow publishers to handle SeenThis Brand Stories ads. The module will handle communication with the ad iframe and resize the ad iframe accurately and handle fullscreen mode according to product specification.
9+
10+
This will allow publishers to safely run the ad format without the need to disable Safeframe when using Prebid.js.

modules/seenthisBrandStories.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { getBoundingClientRect } from "../libraries/boundingClientRect/boundingClientRect.js";
2+
import { getWinDimensions } from "../src/utils.js";
3+
4+
export const DEFAULT_MARGINS = "16px";
5+
6+
export const EVENTS = [
7+
"@seenthis_storylines/ready",
8+
"@seenthis_enabled",
9+
"@seenthis_disabled",
10+
"@seenthis_metric",
11+
"@seenthis_detach",
12+
"@seenthis_modal/opened",
13+
"@seenthis_modal/closed",
14+
"@seenthis_modal/beforeopen",
15+
"@seenthis_modal/beforeclose",
16+
];
17+
18+
const frameElements: Record<string, HTMLIFrameElement> = {};
19+
const containerElements: Record<string, HTMLDivElement> = {};
20+
const isInitialized: Record<string, boolean> = {};
21+
let classNames: Record<string, string> = {};
22+
23+
export function calculateMargins(element: HTMLElement) {
24+
const boundingClientRect = getBoundingClientRect(element);
25+
const wrapperLeftMargin = window.getComputedStyle(element).marginLeft;
26+
const marginLeft = boundingClientRect.left - parseInt(wrapperLeftMargin, 10);
27+
28+
if (boundingClientRect.width === 0 || marginLeft === 0) {
29+
element.style.setProperty("--storylines-margins", DEFAULT_MARGINS);
30+
element.style.setProperty("--storylines-margin-left", DEFAULT_MARGINS);
31+
return;
32+
}
33+
34+
element.style.setProperty("--storylines-margin-left", `-${marginLeft}px`);
35+
element.style.setProperty("--storylines-margins", `${marginLeft * 2}px`);
36+
}
37+
38+
export function getFrameByEvent(event: MessageEvent) {
39+
return Array.from(document.getElementsByTagName("iframe")).filter(
40+
(iframe) => {
41+
return iframe.contentWindow === event.source;
42+
}
43+
)[0];
44+
}
45+
46+
export function addStyleToSingleChildAncestors(
47+
element: HTMLElement,
48+
{ key, value }: { key: string; value: string }
49+
) {
50+
const windowWidth = getWinDimensions().width;
51+
const elementWidth = element.offsetWidth;
52+
53+
if (key in element.style && elementWidth < windowWidth) {
54+
element.style.setProperty(key, value);
55+
}
56+
if (!element.parentElement || element.parentElement?.children.length > 1) {
57+
return;
58+
}
59+
addStyleToSingleChildAncestors(element.parentElement, { key, value });
60+
}
61+
62+
export function findAdWrapper(target: HTMLDivElement) {
63+
return target?.parentElement?.parentElement;
64+
}
65+
66+
export function applyFullWidth(target: HTMLDivElement) {
67+
const adWrapper = findAdWrapper(target);
68+
if (adWrapper) {
69+
addStyleToSingleChildAncestors(adWrapper, { key: "width", value: "100%" });
70+
}
71+
}
72+
73+
export function applyAutoHeight(target: HTMLDivElement) {
74+
const adWrapper = findAdWrapper(target);
75+
if (adWrapper) {
76+
addStyleToSingleChildAncestors(adWrapper, { key: "height", value: "auto" });
77+
addStyleToSingleChildAncestors(adWrapper, {
78+
key: "min-height",
79+
value: "auto",
80+
});
81+
}
82+
}
83+
84+
// listen to messages from iframes
85+
window.addEventListener("message", (event) => {
86+
if (!["https://video.seenthis.se"].includes(event?.origin)) return;
87+
88+
const data = event?.data;
89+
if (!data) return;
90+
91+
switch (data.type) {
92+
case "storylines:init": {
93+
const storyKey = data.storyKey;
94+
if (!storyKey || isInitialized[storyKey]) return;
95+
96+
isInitialized[storyKey] = true;
97+
98+
frameElements[storyKey] = getFrameByEvent(event);
99+
containerElements[storyKey] = frameElements[storyKey]
100+
?.parentElement as HTMLDivElement;
101+
event.source?.postMessage(
102+
"storylines:init-ok",
103+
"*" as WindowPostMessageOptions
104+
);
105+
106+
const styleEl = document.createElement("style");
107+
styleEl.textContent = data.css;
108+
document.head.appendChild(styleEl);
109+
if (data.fixes.includes("full-width")) {
110+
applyFullWidth(containerElements[storyKey]);
111+
}
112+
if (data.fixes.includes("auto-height")) {
113+
applyAutoHeight(containerElements[storyKey]);
114+
}
115+
116+
classNames = data.classNames;
117+
containerElements[storyKey]?.classList.add(classNames.container);
118+
calculateMargins(containerElements[storyKey]);
119+
break;
120+
}
121+
case "@seenthis_modal/beforeopen": {
122+
const storyKey = data.detail.storyKey;
123+
document.body.classList.add(classNames.expandedBody);
124+
containerElements[storyKey]?.classList.add("expanded");
125+
break;
126+
}
127+
case "@seenthis_modal/beforeclose": {
128+
const storyKey = data.detail.storyKey;
129+
document.body.classList.remove(classNames.expandedBody);
130+
containerElements[storyKey]?.classList.remove("expanded");
131+
break;
132+
}
133+
}
134+
135+
// dispatch events to parent window
136+
if (EVENTS.includes(data.type)) {
137+
window.dispatchEvent(new CustomEvent(data.type, { detail: data }));
138+
}
139+
});
140+
141+
Array.from(window.frames).forEach((frame) => {
142+
frame.postMessage("storylines:bridge-ready", "*");
143+
});

0 commit comments

Comments
 (0)