@pexip-engage-public/plugin 1.1.26 → 1.1.27-canary-20250729085737

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -675,15 +675,15 @@ export declare const PluginStateSchema: z.ZodObject<{
675
675
  subject: z.ZodOptional<z.ZodLiteral<true>>;
676
676
  }, "strip", z.ZodTypeAny, {
677
677
  questions?: true | undefined;
678
+ office?: true | undefined;
678
679
  meetingType?: true | undefined;
679
680
  employee?: true | undefined;
680
- office?: true | undefined;
681
681
  subject?: true | undefined;
682
682
  }, {
683
683
  questions?: true | undefined;
684
+ office?: true | undefined;
684
685
  meetingType?: true | undefined;
685
686
  employee?: true | undefined;
686
- office?: true | undefined;
687
687
  subject?: true | undefined;
688
688
  }>>;
689
689
  subjectGroups: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -858,9 +858,9 @@ export declare const PluginStateSchema: z.ZodObject<{
858
858
  schedulable: boolean;
859
859
  skipped: {
860
860
  questions?: true | undefined;
861
+ office?: true | undefined;
861
862
  meetingType?: true | undefined;
862
863
  employee?: true | undefined;
863
- office?: true | undefined;
864
864
  subject?: true | undefined;
865
865
  };
866
866
  customer?: {
@@ -896,6 +896,7 @@ export declare const PluginStateSchema: z.ZodObject<{
896
896
  questions?: Record<string, string | string[]> | undefined;
897
897
  subjectGroups?: string[] | undefined;
898
898
  subjects?: string[] | undefined;
899
+ metadata?: Record<string, unknown> | undefined;
899
900
  appointmentId?: string | undefined;
900
901
  callbackRequestId?: string | undefined;
901
902
  employeeId?: string | undefined;
@@ -907,7 +908,6 @@ export declare const PluginStateSchema: z.ZodObject<{
907
908
  leadSegmentId?: string | undefined;
908
909
  listingId?: string | undefined;
909
910
  meetingType?: "VIDEO" | "PHONE" | "ON_LOCATION" | "OFFICE" | undefined;
910
- metadata?: Record<string, unknown> | undefined;
911
911
  officeId?: string | undefined;
912
912
  subjectId?: string | undefined;
913
913
  warning?: string | undefined;
@@ -1067,6 +1067,7 @@ export declare const PluginStateSchema: z.ZodObject<{
1067
1067
  }) & {
1068
1068
  timeZone?: string | undefined;
1069
1069
  }) | undefined;
1070
+ metadata?: Record<string, unknown> | undefined;
1070
1071
  appointmentId?: string | undefined;
1071
1072
  callbackRequestId?: string | undefined;
1072
1073
  employeeId?: string | undefined;
@@ -1081,14 +1082,13 @@ export declare const PluginStateSchema: z.ZodObject<{
1081
1082
  leadSegmentId?: string | undefined;
1082
1083
  listingId?: string | undefined;
1083
1084
  meetingType?: "VIDEO" | "PHONE" | "ON_LOCATION" | "OFFICE" | undefined;
1084
- metadata?: Record<string, unknown> | undefined;
1085
1085
  officeId?: string | undefined;
1086
1086
  schedulable?: boolean | undefined;
1087
1087
  skipped?: {
1088
1088
  questions?: true | undefined;
1089
+ office?: true | undefined;
1089
1090
  meetingType?: true | undefined;
1090
1091
  employee?: true | undefined;
1091
- office?: true | undefined;
1092
1092
  subject?: true | undefined;
1093
1093
  } | undefined;
1094
1094
  subjectId?: string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pexip-engage-public/plugin",
3
- "version": "1.1.26",
3
+ "version": "1.1.27-canary-20250729085737",
4
4
  "homepage": "https://github.com/skedify/frontend-mono/tree/develop/apps/booking-plugin/packages/plugin-public#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/skedify/frontend-mono/issues"
@@ -22,12 +22,14 @@
22
22
  "./configuration": "./dist/configuration/index.js",
23
23
  "./configuration-parser": "./dist/configuration-parser/index.js",
24
24
  "./configuration-parser/migrate": "./dist/configuration-parser/migrate-legacy-configuration.js",
25
- "./events": "./dist/events/index.js",
26
- "./instance": "./dist/instance/index.js",
27
- "./state": "./dist/state/index.js",
28
25
  "./constants": "./dist/constants.js",
29
26
  "./encoding": "./dist/encoding.js",
30
- "./logger": "./dist/logger.js"
27
+ "./events": "./dist/events/index.js",
28
+ "./instance": "./dist/instance/index.js",
29
+ "./logger": "./dist/logger.js",
30
+ "./resizer-child": "./dist/resizer/child.js",
31
+ "./resizer-parent": "./dist/resizer/parent.js",
32
+ "./state": "./dist/state/index.js"
31
33
  },
32
34
  "files": [
33
35
  "dist",
@@ -35,7 +37,6 @@
35
37
  "src"
36
38
  ],
37
39
  "dependencies": {
38
- "iframe-resizer": "4.3.11",
39
40
  "skedify-uri-encoding": "^2.1.2",
40
41
  "zod": "^3.25.76",
41
42
  "@pexip-engage-public/graphql": "1.1.9",
@@ -45,8 +46,8 @@
45
46
  "@total-typescript/ts-reset": "^0.6.1",
46
47
  "happy-dom": "^18.0.1",
47
48
  "vitest": "^3.2.4",
48
- "eslint-config-pexip-engage": "1.1.27",
49
- "@pexip-engage/tsconfig": "0.1.1"
49
+ "@pexip-engage/tsconfig": "0.1.1",
50
+ "eslint-config-pexip-engage": "1.1.27"
50
51
  },
51
52
  "volta": {
52
53
  "extends": "../../../../package.json"
@@ -1,2 +1,16 @@
1
+ import type { IFrameMessage, IFrameObjectMessage } from "./event-types.js";
2
+
1
3
  // biome-ignore lint/performance/noReExportAll: types only, ignore
2
4
  export * from "./event-types.js";
5
+ export const IFRAME_CHILD_MESSAGE = "iframe-child-message";
6
+ export const IFRAME_PARENT_MESSAGE = "iframe-parent-message";
7
+
8
+ export type IframeChildMessageEventData = {
9
+ type: typeof IFRAME_CHILD_MESSAGE;
10
+ message: IFrameObjectMessage;
11
+ };
12
+
13
+ export type IframeParentMessageEventData = {
14
+ type: typeof IFRAME_PARENT_MESSAGE;
15
+ message: IFrameMessage;
16
+ };
@@ -1,22 +1,73 @@
1
+ import {
2
+ IFRAME_CHILD_MESSAGE,
3
+ IFRAME_PARENT_MESSAGE,
4
+ type IFrameMessage,
5
+ type IFrameObjectMessage,
6
+ } from "../events/index.js";
7
+ import { initialize } from "../resizer/parent.js";
8
+
1
9
  const SPINNER_STYLE =
2
10
  "<style>@keyframes spin{to{transform: rotate(360deg);}}.container{display: flex; align-items: center; justify-content: center; height: calc(700px - 1rem);}svg{fill: rgb(10 33 54); color: rgb(229 231 235); animation: spin 1s linear infinite;}</style>";
3
11
  const SPINNER_DOM = `<div class="container"><svg aria-hidden="true" fill="none" viewBox="0 0 100 101" width="24px" height="24px" xmlns="http://www.w3.org/2000/svg"> <path fill="currentColor" d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" ></path> <path fill="currentFill" d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"></path></svg></div>`;
4
12
 
5
13
  const spinnerHtml = `<!DOCTYPE html><html><head>${SPINNER_STYLE}</head><body>${SPINNER_DOM}</body></html>`;
6
-
7
- const spinnerTemplate = document.createElement("template");
8
- spinnerTemplate.innerHTML = `${SPINNER_STYLE}${SPINNER_DOM}`;
9
-
10
14
  class PexipEngagePluginFrame extends HTMLElement {
11
15
  readonly #shadowRoot: ShadowRoot;
16
+ static observedAttributes = ["src"];
17
+ #cleanup: (() => void) | null = null;
18
+ #iframe: HTMLIFrameElement | null = null;
19
+
12
20
  constructor() {
13
21
  super();
22
+
14
23
  this.#shadowRoot = this.attachShadow({ mode: "open" });
15
- this.#shadowRoot.appendChild(spinnerTemplate.content.cloneNode(true));
16
24
  }
17
25
 
18
- createPexipPlugin(src: string): HTMLIFrameElement {
26
+ connectedCallback() {
27
+ console.log("PexipEngagePluginFrame connected");
28
+ }
29
+
30
+ disconnectedCallback() {
31
+ console.log("PexipEngagePluginFrame disconnected");
32
+ }
33
+
34
+ connectedMoveCallback() {
35
+ console.log("PexipEngagePluginFrame moved");
36
+ }
37
+
38
+ adoptedCallback() {
39
+ console.log("PexipEngagePluginFrame adopted");
40
+ }
41
+
42
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
43
+ console.log(`Attribute ${name} has changed from ${oldValue} to ${newValue}.`);
44
+ }
45
+
46
+ postMessage(message: IFrameObjectMessage) {
47
+ this.#iframe?.contentWindow?.postMessage({ message, type: IFRAME_CHILD_MESSAGE }, "*");
48
+ }
49
+
50
+ validateMessageEvent(event: MessageEvent) {
51
+ const isTargetIframe =
52
+ Boolean(this.#iframe?.contentWindow && this.#iframe.contentWindow === event.source) &&
53
+ event.data.type === IFRAME_PARENT_MESSAGE;
54
+
55
+ if (!isTargetIframe) {
56
+ return null;
57
+ }
58
+
59
+ const message = event.data.message as IFrameMessage;
60
+
61
+ return message;
62
+ }
63
+
64
+ createPexipPlugin() {
65
+ this.#cleanup?.();
66
+ const src = this.getAttribute("src");
67
+ if (!src) throw new Error("Source URL is required to create the Pexip Engage Plugin frame.");
68
+
19
69
  const iframe = document.createElement("iframe");
70
+ this.#iframe = iframe;
20
71
  iframe.src = src;
21
72
  iframe.style.border = "0px";
22
73
  iframe.style.overflow = "hidden";
@@ -28,14 +79,21 @@ class PexipEngagePluginFrame extends HTMLElement {
28
79
  iframe.referrerPolicy = "strict-origin-when-cross-origin";
29
80
  iframe.allow = "clipboard-write; geolocation";
30
81
 
31
- // iframe.loading = "lazy";
32
82
  iframe.onload = () => iframe.removeAttribute("srcdoc");
33
83
  iframe.srcdoc = spinnerHtml;
34
84
 
35
85
  this.#shadowRoot.innerHTML = "";
36
86
  this.#shadowRoot.appendChild(iframe);
37
87
 
38
- return iframe;
88
+ const result = initialize(iframe);
89
+ this.#cleanup = () => {
90
+ result.unsubscribe();
91
+ };
92
+ }
93
+
94
+ unsubscribe() {
95
+ this.#cleanup?.();
96
+ this.#cleanup = null;
39
97
  }
40
98
  }
41
99
 
@@ -1,9 +1,4 @@
1
1
  import { getCurrentPosition } from "@pexip-engage-public/utils/get-current-position";
2
- import resizer, {
3
- type IFrameComponent,
4
- type IFrameMessageData,
5
- } from "iframe-resizer/js/iframeResizer.js";
6
-
7
2
  import { parsePluginConfiguration } from "../configuration-parser/index.js";
8
3
  import { encodeURIParameters, pluginSearchParams } from "../encoding.js";
9
4
  import type {
@@ -27,13 +22,12 @@ import { getPexipEngagePluginFrame } from "./PexipEngagePluginFrame.js";
27
22
 
28
23
  type PluginEventListener = (event: PluginCustomEvent) => unknown;
29
24
 
30
- const PexipEngagePluginFrame = getPexipEngagePluginFrame();
31
-
32
25
  export class PluginInstance {
33
- #instance: IFrameComponent;
34
26
  readonly #target: HTMLElement;
27
+ #element: InstanceType<ReturnType<typeof getPexipEngagePluginFrame>> | null = null;
35
28
  #state: StateUpdateMessage["payload"] | null = null;
36
29
  #meta: Record<string, unknown> = {};
30
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Not in use currently
37
31
  #status: "pending" | "success" | "error" | "disposed" = "pending";
38
32
  #fallbackTimeoutId: number | null = null;
39
33
  readonly #fallbackHTML: string;
@@ -64,7 +58,7 @@ export class PluginInstance {
64
58
  this.#fallbackHTML = existingInstance ? existingInstance.#fallbackHTML : this.#target.innerHTML;
65
59
  existingInstance?.dispose();
66
60
 
67
- this.#instance = this.#createInstance();
61
+ this.#createInstance();
68
62
  PluginInstance.#instances.push(this);
69
63
  queueMacroTask(() => {
70
64
  dispatchEvent({
@@ -76,7 +70,10 @@ export class PluginInstance {
76
70
  });
77
71
  }
78
72
 
79
- #handleMessage = async ({ message }: IFrameMessageData) => {
73
+ #handleMessage = async (event: MessageEvent) => {
74
+ const message = this.#element?.validateMessageEvent(event);
75
+ if (!message) return;
76
+
80
77
  if (message.type === "STATE_UPDATE") {
81
78
  this.#state = message.payload;
82
79
 
@@ -114,7 +111,7 @@ export class PluginInstance {
114
111
  });
115
112
  }
116
113
 
117
- this.#instance.iFrameResizer.sendMessage({
114
+ this.#element?.postMessage({
118
115
  payload: { meta: this.#meta },
119
116
  type: "PRE_APPOINTMENT_REQUEST",
120
117
  });
@@ -123,7 +120,7 @@ export class PluginInstance {
123
120
  }
124
121
 
125
122
  if (message.type === "REQUEST_ORIGIN_URL") {
126
- this.#instance.iFrameResizer.sendMessage({
123
+ this.#element?.postMessage({
127
124
  payload: { href: window.location.href },
128
125
  type: "REQUEST_ORIGIN_URL",
129
126
  });
@@ -134,7 +131,7 @@ export class PluginInstance {
134
131
  if (message.type === "REQUEST_GEOLOCATION") {
135
132
  const geolocation = getCurrentPosition();
136
133
 
137
- this.#instance.iFrameResizer.sendMessage({
134
+ this.#element?.postMessage({
138
135
  payload: await geolocation.catch((error) =>
139
136
  typeof error === "string" ? { error } : { error: "Unknown error" },
140
137
  ),
@@ -174,44 +171,38 @@ export class PluginInstance {
174
171
  };
175
172
 
176
173
  #createInstance() {
174
+ const searchParams = pluginSearchParams.encode(this.#config.config);
175
+ const src = `${PluginInstance.#url}/plugin?${searchParams}`;
176
+
177
+ const PexipEngagePluginFrame = getPexipEngagePluginFrame();
177
178
  const container = new PexipEngagePluginFrame();
179
+ container.setAttribute("src", src);
180
+
181
+ this.#element = container;
178
182
  this.#target.innerHTML = "";
179
183
  this.#target.appendChild(container);
180
184
 
181
- const searchParams = pluginSearchParams.encode(this.#config.config);
182
- const src = `${PluginInstance.#url}/plugin?${searchParams}`;
183
-
184
- const iframe = container.createPexipPlugin(src);
185
- const self = this;
186
- const [instance] = resizer(
187
- {
188
- checkOrigin: false,
189
- heightCalculationMethod: "taggedElement",
190
- log: false,
191
- onInit() {
192
- self.#status = "success";
193
- dispatchEvent({
194
- bubbles: true,
195
- cancelable: true,
196
- detail: { instance: self, type: PluginInstance.EVENT_LOADED },
197
- target: self.#target,
198
- });
199
- },
200
- onMessage: this.#handleMessage,
201
- resizeFrom: "child",
202
- },
203
- iframe,
204
- );
185
+ try {
186
+ container.createPexipPlugin();
205
187
 
206
- if (!instance) throw new Error("Failed to create resizer instance");
188
+ this.#status = "success";
189
+ dispatchEvent({
190
+ bubbles: true,
191
+ cancelable: true,
192
+ detail: { instance: this, type: PluginInstance.EVENT_LOADED },
193
+ target: this.#target,
194
+ });
207
195
 
208
- return instance;
196
+ window.addEventListener("message", this.#handleMessage);
197
+ } catch (err) {
198
+ throw new Error("Failed to create resizer instance");
199
+ }
209
200
  }
210
201
 
211
202
  #restart = () => {
203
+ window.removeEventListener("message", this.#handleMessage);
212
204
  // leave event listeners intact.
213
- this.#instance.iFrameResizer.close();
214
- this.#instance = this.#createInstance();
205
+ this.#createInstance();
215
206
  };
216
207
 
217
208
  /** Destroy the instance */
@@ -226,7 +217,8 @@ export class PluginInstance {
226
217
  window.clearTimeout(this.#fallbackTimeoutId);
227
218
  }
228
219
 
229
- this.#instance.iFrameResizer.close();
220
+ window.removeEventListener("message", this.#handleMessage);
221
+ this.#element?.unsubscribe();
230
222
  const idx = PluginInstance.#instances.indexOf(this);
231
223
  if (idx !== -1) {
232
224
  PluginInstance.#instances.splice(idx, 1);
@@ -238,11 +230,11 @@ export class PluginInstance {
238
230
  setCSSVariable = (name: string, value: string) => {
239
231
  warnPrivate({ name: "setCSSVariable", type: "function" });
240
232
 
241
- this.#instance.iFrameResizer.sendMessage({ payload: { name, value }, type: "CSS_VAR_UPDATE" });
233
+ this.#element?.postMessage({ payload: { name, value }, type: "CSS_VAR_UPDATE" });
242
234
  };
243
235
 
244
236
  setCustomCSS = (css: string) => {
245
- this.#instance.iFrameResizer.sendMessage({ payload: { css }, type: "CUSTOM_CSS_UPDATE" });
237
+ this.#element?.postMessage({ payload: { css }, type: "CUSTOM_CSS_UPDATE" });
246
238
  };
247
239
 
248
240
  #listeners = new Set<EventListener>();
@@ -0,0 +1,138 @@
1
+ import type {
2
+ IFrameMessage,
3
+ IFrameObjectMessage,
4
+ IframeChildMessageEventData,
5
+ IframeParentMessageEventData,
6
+ } from "../events/index.js";
7
+ import {
8
+ type IframeChildInitEventData,
9
+ type IframeResizeEventData,
10
+ type IframeScrollData,
11
+ isBrowser,
12
+ } from "./common.js";
13
+
14
+ export class IframeChildInstance {
15
+ #queue: IFrameMessage[] = [];
16
+ #isQueueConsumed = false;
17
+ #initialized = false;
18
+ #_resizeObserver: ResizeObserver | null = null;
19
+ get #resizeObserver() {
20
+ if (!this.#_resizeObserver) {
21
+ this.#_resizeObserver = new ResizeObserver((entries) => {
22
+ if (!entries[0]?.target) {
23
+ return;
24
+ }
25
+
26
+ const clientRect = entries[0].target.getBoundingClientRect();
27
+ const height = Math.ceil(clientRect.height);
28
+ const width = Math.ceil(clientRect.width);
29
+
30
+ const data: IframeResizeEventData = {
31
+ height,
32
+ type: "iframe-resized",
33
+ width,
34
+ };
35
+
36
+ window.parent.postMessage(data, "*");
37
+ });
38
+ }
39
+
40
+ return this.#_resizeObserver;
41
+ }
42
+
43
+ #handleInitializeSignal = (event: MessageEvent<IframeChildInitEventData>) => {
44
+ const elementToObserve = document.documentElement;
45
+
46
+ if (this.#initialized || window.parent !== event.source) {
47
+ return;
48
+ }
49
+
50
+ this.#resizeObserver.disconnect();
51
+ this.#resizeObserver.observe(elementToObserve);
52
+ this.#initialized = true;
53
+ this.#isQueueConsumed = true;
54
+ this.#queue.forEach((message) => this.sendMessage(message));
55
+ this.#queue.length = 0;
56
+
57
+ this.#onReady();
58
+ };
59
+
60
+ sendMessage = (message: IFrameMessage) => {
61
+ if (!this.#isQueueConsumed && isInIframe()) {
62
+ this.#queue.push(message);
63
+
64
+ return;
65
+ }
66
+
67
+ const data: IframeParentMessageEventData = {
68
+ message,
69
+ type: "iframe-parent-message",
70
+ };
71
+
72
+ this.#sendMessage(data);
73
+ };
74
+
75
+ scrollToOffset = (offset: { top: number }) => {
76
+ const data: IframeScrollData = {
77
+ top: offset.top,
78
+ type: "iframe-scroll-to-offset",
79
+ };
80
+
81
+ this.#sendMessage(data);
82
+ };
83
+
84
+ #sendMessage(data: IframeScrollData | IframeParentMessageEventData) {
85
+ if (!isBrowser() || !isInIframe()) return;
86
+
87
+ window.parent.postMessage(data, "*");
88
+ }
89
+
90
+ #handleMessage = (
91
+ event: MessageEvent<IframeChildInitEventData | IframeChildMessageEventData>,
92
+ ) => {
93
+ if (event.data?.type === "iframe-child-init") {
94
+ return deferWhenWindowDocumentIsLoaded(() =>
95
+ this.#handleInitializeSignal(event as MessageEvent<IframeChildInitEventData>),
96
+ );
97
+ }
98
+
99
+ if (event.data?.type === "iframe-child-message") {
100
+ this.#onMessage(event.data.message);
101
+ }
102
+ };
103
+
104
+ readonly #onReady: () => void;
105
+ readonly #onMessage: (message: IFrameObjectMessage) => void;
106
+
107
+ constructor({
108
+ onReady,
109
+ onMessage,
110
+ }: {
111
+ onReady: () => void;
112
+ onMessage: (message: IFrameObjectMessage) => void;
113
+ }) {
114
+ this.#onReady = onReady;
115
+ this.#onMessage = onMessage;
116
+ }
117
+
118
+ initialize = () => {
119
+ if (!isBrowser() || !isInIframe()) return;
120
+
121
+ window.addEventListener("message", this.#handleMessage);
122
+ };
123
+
124
+ unsubscribe = () => {
125
+ window.removeEventListener("message", this.#handleMessage);
126
+ this.#resizeObserver.disconnect();
127
+ };
128
+ }
129
+
130
+ function isInIframe() {
131
+ return window.self !== window.top;
132
+ }
133
+
134
+ function deferWhenWindowDocumentIsLoaded(executable: () => void) {
135
+ window.document.readyState === "complete"
136
+ ? executable()
137
+ : window.addEventListener("load", executable);
138
+ }
@@ -0,0 +1,28 @@
1
+ export function isBrowser() {
2
+ return typeof window !== "undefined";
3
+ }
4
+
5
+ export type InitializeResult = { unsubscribe: () => void };
6
+
7
+ export type IframeResizeEventData = {
8
+ type: "iframe-resized";
9
+ width: number;
10
+ height?: number;
11
+ };
12
+
13
+ export type IframeScrollData = {
14
+ type: "iframe-scroll-to-offset";
15
+ top: number;
16
+ };
17
+
18
+ export type IframeChildInitEventData = {
19
+ type: "iframe-child-init";
20
+ };
21
+
22
+ export type IframeResizeEvent = MessageEvent<IframeResizeEventData>;
23
+ export type IframeScrollEvent = MessageEvent<IframeScrollData>;
24
+
25
+ export interface RegisteredElement {
26
+ iframe: HTMLIFrameElement;
27
+ initContext: { isInitialized: boolean; retryAttempts: number; retryTimeoutId?: number };
28
+ }
@@ -0,0 +1,102 @@
1
+ import {
2
+ type IframeChildInitEventData,
3
+ type IframeResizeEvent,
4
+ type IframeScrollEvent,
5
+ type InitializeResult,
6
+ isBrowser,
7
+ type RegisteredElement,
8
+ } from "./common.js";
9
+
10
+ export function initialize(iframe: HTMLIFrameElement): InitializeResult {
11
+ if (!isBrowser()) {
12
+ return { unsubscribe() {} };
13
+ }
14
+
15
+ const registeredElement: RegisteredElement = {
16
+ iframe,
17
+ initContext: { isInitialized: false, retryAttempts: 0 },
18
+ };
19
+ const unsubscribe = addCrossOriginChildResizeListener(registeredElement);
20
+
21
+ return { unsubscribe };
22
+ }
23
+
24
+ function addCrossOriginChildResizeListener(registeredElement: RegisteredElement) {
25
+ const { iframe, initContext } = registeredElement;
26
+
27
+ function handleIframeResizedMessage(event: MessageEvent) {
28
+ const isIframeTarget = iframe.contentWindow === event.source;
29
+
30
+ if (!isIframeTarget) {
31
+ return;
32
+ }
33
+
34
+ if (event.data?.type === "iframe-resized") {
35
+ const { height } = (event as IframeResizeEvent).data;
36
+ height && resizeIframe({ newHeight: height, registeredElement });
37
+
38
+ return;
39
+ }
40
+
41
+ if (event.data?.type === "iframe-scroll-to-offset") {
42
+ const { top } = (event as IframeScrollEvent).data;
43
+ const iFramePosition = iframe.getBoundingClientRect();
44
+
45
+ window.scrollTo({ top: iFramePosition.top + window.scrollY + top });
46
+
47
+ return;
48
+ }
49
+ }
50
+
51
+ window.addEventListener("message", handleIframeResizedMessage);
52
+
53
+ const initMessage: IframeChildInitEventData = { type: "iframe-child-init" };
54
+
55
+ function sendInitializationMessageToChild() {
56
+ postMessageSafelyToCrossOriginIframe(iframe, () =>
57
+ iframe.contentWindow?.postMessage(initMessage, "*"),
58
+ );
59
+ initContext.retryAttempts++;
60
+ initContext.retryTimeoutId = window.setTimeout(
61
+ sendInitializationMessageToChild,
62
+ getExponentialBackoffDelay(initContext.retryAttempts),
63
+ );
64
+ }
65
+ sendInitializationMessageToChild();
66
+
67
+ return () => window.removeEventListener("message", handleIframeResizedMessage);
68
+ }
69
+
70
+ function resizeIframe({
71
+ registeredElement,
72
+ newHeight,
73
+ }: {
74
+ registeredElement: RegisteredElement;
75
+ newHeight: number;
76
+ }) {
77
+ const { iframe, initContext } = registeredElement;
78
+ if (!initContext.isInitialized) {
79
+ initContext.isInitialized = true;
80
+ clearTimeout(initContext.retryTimeoutId);
81
+ }
82
+
83
+ iframe.style.height = `${newHeight}px`;
84
+ }
85
+
86
+ /** Post the message twice, it assures the target to receive the message at least once */
87
+ function postMessageSafelyToCrossOriginIframe(iframe: HTMLIFrameElement, executable: () => void) {
88
+ executable();
89
+ iframe.addEventListener("load", executable);
90
+ }
91
+
92
+ function getExponentialBackoffDelay(nthRetry: number) {
93
+ if (nthRetry <= 100) {
94
+ return 100; // for 10 seconds
95
+ }
96
+
97
+ if (nthRetry <= 120) {
98
+ return 1000; // for 20 seconds
99
+ }
100
+
101
+ return 10000;
102
+ }