• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thoughtspot / visual-embed-sdk / #3092

17 Dec 2025 04:14AM UTC coverage: 94.318% (-0.05%) from 94.364%
#3092

push

web-flow
New logo on Readme.md

1701 of 1903 branches covered (89.39%)

Branch coverage included in aggregate %.

3246 of 3342 relevant lines covered (97.13%)

113.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.47
/src/embed/ts-embed.ts
1
/**
2
 * Copyright (c) 2022
3
 *
4
 * Base classes
5
 * @summary Base classes
6
 * @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
7
 */
8

9
import isEqual from 'lodash/isEqual';
14✔
10
import isEmpty from 'lodash/isEmpty';
14✔
11
import isObject from 'lodash/isObject';
14✔
12
import {
14✔
13
    TriggerPayload,
14
    TriggerResponse,
15
    UIPassthroughArrayResponse,
16
    UIPassthroughEvent,
17
    UIPassthroughRequest,
18
} from './hostEventClient/contracts';
19
import { logger } from '../utils/logger';
14✔
20
import { getAuthenticationToken } from '../authToken';
14✔
21
import { AnswerService } from '../utils/graphql/answerService/answerService';
14✔
22
import {
14✔
23
    getEncodedQueryParamsString,
24
    getCssDimension,
25
    getOffsetTop,
26
    embedEventStatus,
27
    setAttributes,
28
    getCustomisations,
29
    getRuntimeFilters,
30
    getDOMNode,
31
    getFilterQuery,
32
    getQueryParamString,
33
    getRuntimeParameters,
34
    setStyleProperties,
35
    removeStyleProperties,
36
    isUndefined,
37
} from '../utils';
38
import { getCustomActions } from '../utils/custom-actions';
14✔
39
import {
14✔
40
    getThoughtSpotHost,
41
    URL_MAX_LENGTH,
42
    DEFAULT_EMBED_WIDTH,
43
    DEFAULT_EMBED_HEIGHT,
44
    getV2BasePath,
45
} from '../config';
46
import {
14✔
47
    AuthType,
48
    DOMSelector,
49
    HostEvent,
50
    EmbedEvent,
51
    MessageCallback,
52
    Action,
53
    Param,
54
    EmbedConfig,
55
    MessageOptions,
56
    MessageCallbackObj,
57
    ContextMenuTriggerOptions,
58
    DefaultAppInitData,
59
    AllEmbedViewConfig as ViewConfig,
60
    EmbedErrorDetailsEvent,
61
    ErrorDetailsTypes,
62
    EmbedErrorCodes,
63
} from '../types';
64
import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
14✔
65
import { processEventData, processAuthFailure } from '../utils/processData';
14✔
66
import pkgInfo from '../../package.json';
14✔
67
import {
14✔
68
    getAuthPromise, renderInQueue, handleAuth, notifyAuthFailure,
69
    getInitPromise,
70
    getIsInitCalled,
71
} from './base';
72
import { AuthFailureType } from '../auth';
14✔
73
import { getEmbedConfig } from './embedConfig';
14✔
74
import { ERROR_MESSAGE } from '../errors';
14✔
75
import { getPreauthInfo } from '../utils/sessionInfoService';
14✔
76
import { HostEventClient } from './hostEventClient/host-event-client';
14✔
77
import { getInterceptInitData, handleInterceptEvent, processApiInterceptResponse, processLegacyInterceptResponse } from '../api-intercept';
14✔
78

79
const { version } = pkgInfo;
14✔
80

81
/**
82
 * Global prefix for all Thoughtspot postHash Params.
83
 */
84
export const THOUGHTSPOT_PARAM_PREFIX = 'ts-';
14✔
85
const TS_EMBED_ID = '_thoughtspot-embed';
14✔
86

87
/**
88
 * The event id map from v2 event names to v1 event id
89
 * v1 events are the classic embed events implemented in Blink v1
90
 * We cannot rename v1 event types to maintain backward compatibility
91
 * @internal
92
 */
93
const V1EventMap: Record<string, any> = {};
14✔
94

95
/**
96
 * Base class for embedding v2 experience
97
 * Note: the v2 version of ThoughtSpot Blink is built on the new stack:
98
 * React+GraphQL
99
 */
100
export class TsEmbed {
14✔
101
    /**
102
     * The DOM node which was inserted by the SDK to either
103
     * render the iframe or display an error message.
104
     * This is useful for removing the DOM node when the
105
     * embed instance is destroyed.
106
     */
107
    protected insertedDomEl: Node;
108

109
    /**
110
     * The DOM node where the ThoughtSpot app is to be embedded.
111
     */
112
    protected el: HTMLElement;
113

114
    /**
115
     * The key to store the embed instance in the DOM node
116
     */
117
    protected embedNodeKey = '__tsEmbed';
490✔
118

119
    protected isAppInitialized = false;
490✔
120

121
    /**
122
     * A reference to the iframe within which the ThoughtSpot app
123
     * will be rendered.
124
     */
125
    protected iFrame: HTMLIFrameElement;
126

127
    /**
128
     * Setter for the iframe element
129
     * @param {HTMLIFrameElement} iFrame HTMLIFrameElement
130
     */
131
    protected setIframeElement(iFrame: HTMLIFrameElement): void {
132
        this.iFrame = iFrame;
455✔
133
        this.hostEventClient.setIframeElement(iFrame);
455✔
134
    }
135

136
    protected viewConfig: ViewConfig & { visibleTabs?: string[], hiddenTabs?: string[], showAlerts?: boolean };
137

138
    protected embedConfig: EmbedConfig;
139

140
    /**
141
     * The ThoughtSpot hostname or IP address
142
     */
143
    protected thoughtSpotHost: string;
144

145
    /*
146
     * This is the base to access ThoughtSpot V2.
147
     */
148
    protected thoughtSpotV2Base: string;
149

150
    /**
151
     * A map of event handlers for particular message types triggered
152
     * by the embedded app; multiple event handlers can be registered
153
     * against a particular message type.
154
     */
155
    private eventHandlerMap: Map<string, MessageCallbackObj[]>;
156

157
    /**
158
     * A flag that is set to true post render.
159
     */
160
    protected isRendered: boolean;
161

162
    /**
163
     * A flag to mark if an error has occurred.
164
     */
165
    private isError: boolean;
166

167
    /**
168
     * A flag that is set to true post preRender.
169
     */
170
    private isPreRendered: boolean;
171

172
    /**
173
     * Should we encode URL Query Params using base64 encoding which thoughtspot
174
     * will generate for embedding. This provides additional security to
175
     * thoughtspot clusters against Cross site scripting attacks.
176
     * @default false
177
     */
178
    private shouldEncodeUrlQueryParams = false;
490✔
179

180
    private defaultHiddenActions = [Action.ReportError];
490✔
181

182
    private resizeObserver: ResizeObserver;
183

184
    protected hostEventClient: HostEventClient;
185

186
    protected isReadyForRenderPromise;
187

188
    /**
189
     * Handler for fullscreen change events
190
     */
191
    private fullscreenChangeHandler: (() => void) | null = null;
490✔
192

193
    constructor(domSelector: DOMSelector, viewConfig?: ViewConfig) {
194
        this.el = getDOMNode(domSelector);
490✔
195
        this.eventHandlerMap = new Map();
490✔
196
        this.isError = false;
490✔
197
        this.viewConfig = {
490✔
198
            excludeRuntimeFiltersfromURL: false,
199
            excludeRuntimeParametersfromURL: false,
200
            ...viewConfig,
201
        };
202
        this.registerAppInit();
490✔
203
        uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_EMBED_CREATE, {
490✔
204
            ...viewConfig,
205
        });
206
        const embedConfig = getEmbedConfig();
490✔
207
        this.embedConfig = embedConfig;
490✔
208

209
        this.hostEventClient = new HostEventClient(this.iFrame);
490✔
210
        this.isReadyForRenderPromise = getInitPromise().then(async () => {
490✔
211
            if (!embedConfig.authTriggerContainer && !embedConfig.useEventForSAMLPopup) {
490✔
212
                this.embedConfig.authTriggerContainer = domSelector;
89✔
213
            }
214
            this.thoughtSpotHost = getThoughtSpotHost(embedConfig);
490✔
215
            this.thoughtSpotV2Base = getV2BasePath(embedConfig);
490✔
216
            this.shouldEncodeUrlQueryParams = embedConfig.shouldEncodeUrlQueryParams;
490✔
217
        });
218
    }
219

220
    /**
221
     * Throws error encountered during initialization.
222
     */
223
    private throwInitError() {
224
        this.handleError({
1✔
225
            errorType: ErrorDetailsTypes.VALIDATION_ERROR,
226
            message: ERROR_MESSAGE.INIT_SDK_REQUIRED,
227
            code: EmbedErrorCodes.INIT_ERROR,
228
            error : ERROR_MESSAGE.INIT_SDK_REQUIRED,
229
        });
230
    }
231

232
    /**
233
     * Handles errors within the SDK
234
     * @param error The error message or object
235
     * @param errorDetails The error details
236
     */
237
    protected handleError(errorDetails: EmbedErrorDetailsEvent) {
238
        this.isError = true;
15✔
239
        this.executeCallbacks(EmbedEvent.Error, errorDetails);
15✔
240
        // Log error
241
        logger.error(errorDetails);
15✔
242
    }
243

244
    /**
245
     * Extracts the type field from the event payload
246
     * @param event The window message event
247
     */
248
    private getEventType(event: MessageEvent) {
249

250
        return event.data?.type || event.data?.__type;
3,155!
251
    }
252

253
    /**
254
     * Extracts the port field from the event payload
255
     * @param event  The window message event
256
     * @returns
257
     */
258
    private getEventPort(event: MessageEvent) {
259
        if (event.ports.length && event.ports[0]) {
3,155✔
260
            return event.ports[0];
2,402✔
261
        }
262
        return null;
753✔
263
    }
264

265
    /**
266
     * Checks if preauth cache is enabled
267
     * from the view config and embed config
268
     * @returns boolean
269
     */
270
    private isPreAuthCacheEnabled() {
271
        // Disable preauth cache when:
272
        // 1. overrideOrgId is present since:
273
        //    - cached auth info would be for wrong org
274
        //    - info call response changes for each different overrideOrgId
275
        // 2. disablePreauthCache is explicitly set to true
276
        // 3. FullAppEmbed has primary navbar visible since:
277
        //    - primary navbar requires fresh auth state for navigation
278
        //    - cached auth may not reflect current user permissions
279
        const isDisabled = (
280
            this.viewConfig.overrideOrgId !== undefined
461✔
281
            || this.embedConfig.disablePreauthCache === true
282
            || this.isFullAppEmbedWithVisiblePrimaryNavbar()
283
        );
284
        return !isDisabled;
461✔
285
    }
286

287
    /**
288
     * Checks if current embed is FullAppEmbed with visible primary navbar
289
     * @returns boolean
290
     */
291
    private isFullAppEmbedWithVisiblePrimaryNavbar(): boolean {
292
        const appViewConfig = this.viewConfig as any;
459✔
293

294
        // Check if this is a FullAppEmbed (AppEmbed)
295
        // showPrimaryNavbar defaults to true if not explicitly set to false
296
        return (
459✔
297
            appViewConfig.embedComponentType === 'AppEmbed'
609✔
298
            && appViewConfig.showPrimaryNavbar === true
299
        );
300
    }
301

302
    /**
303
     * fix for ts7.sep.cl
304
     * will be removed for ts7.oct.cl
305
     * @param event
306
     * @param eventType
307
     * @hidden
308
     */
309
    private formatEventData(event: MessageEvent, eventType: string) {
310
        const eventData = {
3,155✔
311
            ...event.data,
312
            type: eventType,
313
        };
314
        if (!eventData.data) {
3,155✔
315
            eventData.data = event.data.payload;
529✔
316
        }
317
        return eventData;
3,155✔
318
    }
319

320
    private subscribedListeners: Record<string, any> = {};
490✔
321

322
    /**
323
     * Subscribe to network events (online/offline) that should
324
     * work regardless of auth status
325
     */
326
    private subscribeToNetworkEvents() {
327
        this.unsubscribeToNetworkEvents();
447✔
328

329
        const onlineEventListener = (e: Event) => {
447✔
330
            this.trigger(HostEvent.Reload);
265✔
331
        };
332
        window.addEventListener('online', onlineEventListener);
447✔
333

334
        const offlineEventListener = (e: Event) => {
447✔
335
            const errorDetails = {
×
336
                errorType: ErrorDetailsTypes.NETWORK,
337
                message: ERROR_MESSAGE.OFFLINE_WARNING,
338
                code: EmbedErrorCodes.NETWORK_ERROR,
339
                offlineWarning : ERROR_MESSAGE.OFFLINE_WARNING,
340
            };
341
            this.executeCallbacks(EmbedEvent.Error, errorDetails);
×
342
            logger.warn(errorDetails);
×
343
        };
344
        window.addEventListener('offline', offlineEventListener);
447✔
345

346
        this.subscribedListeners.online = onlineEventListener;
447✔
347
        this.subscribedListeners.offline = offlineEventListener;
447✔
348
    }
349

350
    private handleApiInterceptEvent({ eventData, eventPort }: { eventData: any, eventPort: MessagePort | void }) {
351
        const executeEvent = (_eventType: EmbedEvent, data: any) => {
10✔
352
            this.executeCallbacks(_eventType, data, eventPort);
3✔
353
        }
354
        const getUnsavedAnswerTml = async (props: { sessionId?: string, vizId?: string }) => {
10✔
355
            const response = await this.triggerUIPassThrough(UIPassthroughEvent.GetUnsavedAnswerTML, props);
2✔
356
            return response.filter((item) => item.value)?.[0]?.value;
2!
357
        }
358
        handleInterceptEvent({ eventData, executeEvent, viewConfig: this.viewConfig, getUnsavedAnswerTml });
10✔
359
    }
360

361
    private messageEventListener = (event: MessageEvent<any>) => {
490✔
362
        const eventType = this.getEventType(event);
3,155✔
363
        const eventPort = this.getEventPort(event);
3,155✔
364
        const eventData = this.formatEventData(event, eventType);
3,155✔
365
        if (event.source === this.iFrame.contentWindow) {
3,155✔
366
            const processedEventData = processEventData(
74✔
367
                eventType,
368
                eventData,
369
                this.thoughtSpotHost,
370
                this.isPreRendered ? this.preRenderWrapper : this.el,
74✔
371
            );
372

373
            if (eventType === EmbedEvent.ApiIntercept) {
74✔
374
                this.handleApiInterceptEvent({ eventData, eventPort });
10✔
375
                return;
10✔
376
            }
377

378
            this.executeCallbacks(
64✔
379
                eventType,
380
                processedEventData,
381
                eventPort,
382
            );
383
        }
384
    };
385
    /**
386
     * Subscribe to message events that depend on successful iframe setup
387
     */
388
    private subscribeToMessageEvents() {
389
        this.unsubscribeToMessageEvents();
439✔
390

391
        window.addEventListener('message', this.messageEventListener);
439✔
392

393
        this.subscribedListeners.message = this.messageEventListener;
439✔
394
    }
395
   
396

397
    /**
398
     * Adds event listeners for both network and message events.
399
     * This maintains backward compatibility with the existing method.
400
     * Adds a global event listener to window for "message" events.
401
     * ThoughtSpot detects if a particular event is targeted to this
402
     * embed instance through an identifier contained in the payload,
403
     * and executes the registered callbacks accordingly.
404
     */
405
    private subscribeToEvents() {
406
        this.subscribeToNetworkEvents();
8✔
407
        this.subscribeToMessageEvents();
8✔
408
    }
409

410

411
    private unsubscribeToNetworkEvents() {
412
        if (this.subscribedListeners.online) {
447✔
413
            window.removeEventListener('online', this.subscribedListeners.online);
4✔
414
            delete this.subscribedListeners.online;
4✔
415
        }
416
        if (this.subscribedListeners.offline) {
447✔
417
            window.removeEventListener('offline', this.subscribedListeners.offline);
4✔
418
            delete this.subscribedListeners.offline;
4✔
419
        }
420
    }
421

422
    private unsubscribeToMessageEvents() {
423
        if (this.subscribedListeners.message) {
440✔
424
            window.removeEventListener('message', this.subscribedListeners.message);
5✔
425
            delete this.subscribedListeners.message;
5✔
426
        }
427
    }
428

429
    private unsubscribeToEvents() {
430
        Object.keys(this.subscribedListeners).forEach((key) => {
47✔
431
            window.removeEventListener(key, this.subscribedListeners[key]);
125✔
432
        });
433
    }
434

435
    protected async getAuthTokenForCookielessInit() {
436
        let authToken = '';
26✔
437
        if (this.embedConfig.authType !== AuthType.TrustedAuthTokenCookieless) return authToken;
26✔
438

439
        try {
6✔
440
            authToken = await getAuthenticationToken(this.embedConfig);
6✔
441
        } catch (e) {
442
            processAuthFailure(e, this.isPreRendered ? this.preRenderWrapper : this.el);
2✔
443
            throw e;
2✔
444
        }
445

446
        return authToken;
4✔
447
    }
448

449
    protected async getDefaultAppInitData(): Promise<DefaultAppInitData> {
450
        const authToken = await this.getAuthTokenForCookielessInit();
25✔
451
        const customActionsResult = getCustomActions([
23✔
452
            ...(this.viewConfig.customActions || []),
45✔
453
            ...(this.embedConfig.customActions || [])
46✔
454
        ]);
455
        if (customActionsResult.errors.length > 0) {
23!
456
            this.handleError({
×
457
                    errorType: ErrorDetailsTypes.VALIDATION_ERROR,
458
                    message: customActionsResult.errors,
459
                    code: EmbedErrorCodes.CUSTOM_ACTION_VALIDATION,
460
                    error : { type: EmbedErrorCodes.CUSTOM_ACTION_VALIDATION, message: customActionsResult.errors }
461
                });
462
        }
463
        const baseInitData = {
23✔
464
            customisations: getCustomisations(this.embedConfig, this.viewConfig),
465
            authToken,
466
            runtimeFilterParams: this.viewConfig.excludeRuntimeFiltersfromURL
68✔
467
                ? getRuntimeFilters(this.viewConfig.runtimeFilters)
468
                : null,
469
            runtimeParameterParams: this.viewConfig.excludeRuntimeParametersfromURL
68✔
470
                ? getRuntimeParameters(this.viewConfig.runtimeParameters || [])
9✔
471
                : null,
472
            hiddenHomepageModules: this.viewConfig.hiddenHomepageModules || [],
45✔
473
            reorderedHomepageModules: this.viewConfig.reorderedHomepageModules || [],
45✔
474
            hostConfig: this.embedConfig.hostConfig,
475
            hiddenHomeLeftNavItems: this.viewConfig?.hiddenHomeLeftNavItems
92!
476
                ? this.viewConfig?.hiddenHomeLeftNavItems
3!
477
                : [],
478
            customVariablesForThirdPartyTools:
479
                this.embedConfig.customVariablesForThirdPartyTools || {},
36✔
480
            hiddenListColumns: this.viewConfig.hiddenListColumns || [],
46✔
481
            customActions: customActionsResult.actions,
482
            ...getInterceptInitData(this.viewConfig),
483
        };
484

485
        return baseInitData;
23✔
486
    }
487

488
    protected async getAppInitData() {
489
        return this.getDefaultAppInitData();
25✔
490
    }
491

492
    /**
493
     * Send Custom style as part of payload of APP_INIT
494
     * @param _
495
     * @param responder
496
     */
497
    private appInitCb = async (_: any, responder: any) => {
490✔
498
        try {
21✔
499
            const appInitData = await this.getAppInitData();
21✔
500
            this.isAppInitialized = true;
19✔
501
            responder({
19✔
502
                type: EmbedEvent.APP_INIT,
503
                data: appInitData,
504
            });
505
        } catch (e) {
506
            logger.error(`AppInit failed, Error : ${e?.message}`);
2!
507
        }
508
    };
509

510
    /**
511
     * Sends updated auth token to the iFrame to avoid user logout
512
     * @param _
513
     * @param responder
514
     */
515
    private updateAuthToken = async (_: any, responder: any) => {
490✔
516
        const { authType } = this.embedConfig;
10✔
517
        let { autoLogin } = this.embedConfig;
10✔
518
        // Default autoLogin: true for cookieless if undefined/null, otherwise
519
        // false
520
        autoLogin = autoLogin ?? (authType === AuthType.TrustedAuthTokenCookieless);
10✔
521
        if (autoLogin && authType === AuthType.TrustedAuthTokenCookieless) {
10✔
522
            try {
5✔
523
                const authToken = await getAuthenticationToken(this.embedConfig);
5✔
524
                responder({
3✔
525
                    type: EmbedEvent.AuthExpire,
526
                    data: { authToken },
527
                });
528
            } catch (e) {
529
                logger.error(`${ERROR_MESSAGE.INVALID_TOKEN_ERROR} Error : ${e?.message}`);
2!
530
                processAuthFailure(e, this.isPreRendered ? this.preRenderWrapper : this.el);
2✔
531
            }
532
        } else if (autoLogin) {
5✔
533
            handleAuth();
2✔
534
        }
535
        notifyAuthFailure(AuthFailureType.EXPIRY);
10✔
536
    };
537

538
    /**
539
     * Auto Login and send updated authToken to the iFrame to avoid user session logout
540
     * @param _
541
     * @param responder
542
     */
543
    private idleSessionTimeout = (_: any, responder: any) => {
490✔
544
        handleAuth().then(async () => {
3✔
545
            let authToken = '';
3✔
546
            try {
3✔
547
                authToken = await getAuthenticationToken(this.embedConfig);
3✔
548
                responder({
2✔
549
                    type: EmbedEvent.IdleSessionTimeout,
550
                    data: { authToken },
551
                });
552
            } catch (e) {
553
                logger.error(`${ERROR_MESSAGE.INVALID_TOKEN_ERROR} Error : ${e?.message}`);
1!
554
                processAuthFailure(e, this.isPreRendered ? this.preRenderWrapper : this.el);
1!
555
            }
556
        }).catch((e) => {
557
            logger.error(`Auto Login failed, Error : ${e?.message}`);
×
558
        });
559
        notifyAuthFailure(AuthFailureType.IDLE_SESSION_TIMEOUT);
3✔
560
    };
561

562
    /**
563
     * Register APP_INIT event and sendback init payload
564
     */
565
    private registerAppInit = () => {
490✔
566
        this.on(EmbedEvent.APP_INIT, this.appInitCb, { start: false }, true);
490✔
567
        this.on(EmbedEvent.AuthExpire, this.updateAuthToken, { start: false }, true);
490✔
568
        this.on(EmbedEvent.IdleSessionTimeout, this.idleSessionTimeout, { start: false }, true);
490✔
569

570
        const embedListenerReadyHandler = this.createEmbedContainerHandler(EmbedEvent.EmbedListenerReady);
490✔
571
        this.on(EmbedEvent.EmbedListenerReady, embedListenerReadyHandler, { start: false }, true);
490✔
572

573
        const authInitHandler = this.createEmbedContainerHandler(EmbedEvent.AuthInit);
490✔
574
        this.on(EmbedEvent.AuthInit, authInitHandler, { start: false }, true);
490✔
575
    };
576

577
    /**
578
     * Constructs the base URL string to load the ThoughtSpot app.
579
     * @param query
580
     */
581
    protected getEmbedBasePath(query: string): string {
582
        let queryString = query.startsWith('?') ? query : `?${query}`;
147✔
583
        if (this.shouldEncodeUrlQueryParams) {
147✔
584
            queryString = `?base64UrlEncodedFlags=${getEncodedQueryParamsString(
1✔
585
                queryString.substr(1),
586
            )}`;
587
        }
588
        const basePath = [this.thoughtSpotHost, this.thoughtSpotV2Base, queryString]
147✔
589
            .filter((x) => x.length > 0)
441✔
590
            .join('/');
591

592
        return `${basePath}#`;
147✔
593
    }
594

595
    protected getUpdateEmbedParamsObject() {
596
        let queryParams = this.getEmbedParamsObject();
4✔
597
        queryParams = { ...this.viewConfig, ...queryParams, ...this.getAppInitData() };
4✔
598
        return queryParams;
4✔
599
    }
600

601
    /**
602
     * Common query params set for all the embed modes.
4✔
603
     * @param queryParams
1✔
604
     * @returns queryParams
1✔
605
     */
606
    protected getBaseQueryParams(queryParams: Record<any, any> = {}) {
607
        let hostAppUrl = window?.location?.host || '';
4✔
608

609
        // The below check is needed because TS Cloud firewall, blocks
610
        // localhost/127.0.0.1 in any url param.
611
        if (hostAppUrl.includes('localhost') || hostAppUrl.includes('127.0.0.1')) {
612
            hostAppUrl = 'local-host';
613
        }
614
        const blockNonEmbedFullAppAccess = this.embedConfig.blockNonEmbedFullAppAccess ?? true;
615
        queryParams[Param.EmbedApp] = true;
162✔
616
        queryParams[Param.HostAppUrl] = encodeURIComponent(hostAppUrl);
452!
617
        queryParams[Param.ViewPortHeight] = window.innerHeight;
618
        queryParams[Param.ViewPortWidth] = window.innerWidth;
619
        queryParams[Param.Version] = version;
620
        queryParams[Param.AuthType] = this.embedConfig.authType;
452!
621
        queryParams[Param.blockNonEmbedFullAppAccess] = blockNonEmbedFullAppAccess;
452✔
622
        queryParams[Param.AutoLogin] = this.embedConfig.autoLogin;
623
        if (this.embedConfig.disableLoginRedirect === true || this.embedConfig.autoLogin === true) {
452✔
624
            queryParams[Param.DisableLoginRedirect] = true;
452✔
625
        }
452✔
626
        if (this.embedConfig.authType === AuthType.EmbeddedSSO) {
452✔
627
            queryParams[Param.ForceSAMLAutoRedirect] = true;
452✔
628
        }
452✔
629
        if (this.embedConfig.authType === AuthType.TrustedAuthTokenCookieless) {
452✔
630
            queryParams[Param.cookieless] = true;
452✔
631
        }
452✔
632
        if (this.embedConfig.pendoTrackingKey) {
452✔
633
            queryParams[Param.PendoTrackingKey] = this.embedConfig.pendoTrackingKey;
8✔
634
        }
635
        if (this.embedConfig.numberFormatLocale) {
452!
636
            queryParams[Param.NumberFormatLocale] = this.embedConfig.numberFormatLocale;
×
637
        }
638
        if (this.embedConfig.dateFormatLocale) {
452✔
639
            queryParams[Param.DateFormatLocale] = this.embedConfig.dateFormatLocale;
16✔
640
        }
641
        if (this.embedConfig.currencyFormat) {
452✔
642
            queryParams[Param.CurrencyFormat] = this.embedConfig.currencyFormat;
1✔
643
        }
644

452✔
645
        const {
10✔
646
            disabledActions,
647
            disabledActionReason,
452✔
648
            hiddenActions,
10✔
649
            visibleActions,
650
            hiddenTabs,
452✔
651
            visibleTabs,
10✔
652
            showAlerts,
653
            additionalFlags: additionalFlagsFromView,
654
            locale,
655
            customizations,
656
            contextMenuTrigger,
657
            linkOverride,
658
            insertInToSlide,
659
            disableRedirectionLinksInNewTab,
660
            overrideOrgId,
661
            exposeTranslationIDs,
662
            primaryAction,
663
        } = this.viewConfig;
664

665
        const { additionalFlags: additionalFlagsFromInit } = this.embedConfig;
666

667
        const additionalFlags = {
668
            ...additionalFlagsFromInit,
669
            ...additionalFlagsFromView,
670
        };
671

672
        if (Array.isArray(visibleActions) && Array.isArray(hiddenActions)) {
452✔
673
            this.handleError({
674
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
452✔
675
                message: ERROR_MESSAGE.CONFLICTING_ACTIONS_CONFIG,
676
                code: EmbedErrorCodes.CONFLICTING_ACTIONS_CONFIG,
452✔
677
                error : ERROR_MESSAGE.CONFLICTING_ACTIONS_CONFIG,
678
            });
679
            return queryParams;
680
        }
681

452✔
682
        if (Array.isArray(visibleTabs) && Array.isArray(hiddenTabs)) {
4✔
683
            this.handleError({
684
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
685
                message: ERROR_MESSAGE.CONFLICTING_TABS_CONFIG,
686
                code: EmbedErrorCodes.CONFLICTING_TABS_CONFIG,
687
                error : ERROR_MESSAGE.CONFLICTING_TABS_CONFIG,
688
            });
4✔
689
            return queryParams;
690
        }
691
        if (primaryAction) {
448✔
692
            queryParams[Param.PrimaryAction] = primaryAction;
4✔
693
        }
694

695
        if (disabledActions?.length) {
696
            queryParams[Param.DisableActions] = disabledActions;
697
        }
698
        if (disabledActionReason) {
4✔
699
            queryParams[Param.DisableActionReason] = disabledActionReason;
700
        }
444!
701
        if (exposeTranslationIDs) {
×
702
            queryParams[Param.ExposeTranslationIDs] = exposeTranslationIDs;
703
        }
704
        queryParams[Param.HideActions] = [...this.defaultHiddenActions, ...(hiddenActions ?? [])];
444✔
705
        if (Array.isArray(visibleActions)) {
7✔
706
            queryParams[Param.VisibleActions] = visibleActions;
707
        }
444✔
708
        if (Array.isArray(hiddenTabs)) {
6✔
709
            queryParams[Param.HiddenTabs] = hiddenTabs;
710
        }
444✔
711
        if (Array.isArray(visibleTabs)) {
1✔
712
            queryParams[Param.VisibleTabs] = visibleTabs;
713
        }
444✔
714
        /**
444✔
715
         * Default behavior for context menu will be left-click
6✔
716
         *  from version 9.2.0.cl the user have an option to override context
717
         *  menu click
444✔
718
         */
2✔
719
        if (contextMenuTrigger === ContextMenuTriggerOptions.LEFT_CLICK) {
720
            queryParams[Param.ContextMenuTrigger] = 'left';
444✔
721
        } else if (contextMenuTrigger === ContextMenuTriggerOptions.RIGHT_CLICK) {
1✔
722
            queryParams[Param.ContextMenuTrigger] = 'right';
723
        } else if (contextMenuTrigger === ContextMenuTriggerOptions.BOTH_CLICKS) {
724
            queryParams[Param.ContextMenuTrigger] = 'both';
725
        }
726

727
        const embedCustomizations = this.embedConfig.customizations;
728
        const spriteUrl = customizations?.iconSpriteUrl || embedCustomizations?.iconSpriteUrl;
444✔
729
        if (spriteUrl) {
3✔
730
            queryParams[Param.IconSpriteUrl] = spriteUrl.replace('https://', '');
441✔
731
        }
2✔
732

439✔
733
        const stringIDsUrl = customizations?.content?.stringIDsUrl
2✔
734
            || embedCustomizations?.content?.stringIDsUrl;
735
        if (stringIDsUrl) {
736
            queryParams[Param.StringIDsUrl] = stringIDsUrl;
444✔
737
        }
444✔
738

444✔
739
        if (showAlerts !== undefined) {
1✔
740
            queryParams[Param.ShowAlerts] = showAlerts;
741
        }
742
        if (locale !== undefined) {
444✔
743
            queryParams[Param.Locale] = locale;
2,664✔
744
        }
444✔
745

2✔
746
        if (linkOverride) {
747
            queryParams[Param.LinkOverride] = linkOverride;
748
        }
444✔
749
        if (insertInToSlide) {
1✔
750
            queryParams[Param.ShowInsertToSlide] = insertInToSlide;
751
        }
444✔
752
        if (disableRedirectionLinksInNewTab) {
1✔
753
            queryParams[Param.DisableRedirectionLinksInNewTab] = disableRedirectionLinksInNewTab;
754
        }
755
        if (overrideOrgId !== undefined) {
444!
756
            queryParams[Param.OverrideOrgId] = overrideOrgId;
×
757
        }
758

444!
759
        if (this.isPreAuthCacheEnabled()) {
×
760
            queryParams[Param.preAuthCache] = true;
761
        }
444!
762

×
763
        queryParams[Param.OverrideNativeConsole] = true;
764
        queryParams[Param.ClientLogLevel] = this.embedConfig.logLevel;
444✔
765

3✔
766
        if (isObject(additionalFlags) && !isEmpty(additionalFlags)) {
767
            Object.assign(queryParams, additionalFlags);
768
        }
444✔
769

435✔
770
        // Do not add any flags below this, as we want additional flags to
771
        // override other flags
772

444✔
773
        return queryParams;
444✔
774
    }
775

444✔
776
    /**
8✔
777
     * Constructs the base URL string to load v1 of the ThoughtSpot app.
778
     * This is used for embedding Liveboards, visualizations, and full application.
779
     * @param queryString The query string to append to the URL.
780
     * @param isAppEmbed A Boolean parameter to specify if you are embedding
781
     * the full application.
782
     */
444✔
783
    protected getV1EmbedBasePath(queryString: string): string {
784
        const queryParams = this.shouldEncodeUrlQueryParams
785
            ? `?base64UrlEncodedFlags=${getEncodedQueryParamsString(queryString)}`
786
            : `?${queryString}`;
787
        const host = this.thoughtSpotHost;
788
        const path = `${host}/${queryParams}#`;
789
        return path;
790
    }
791

792
    protected getEmbedParams() {
793
        const queryParams = this.getEmbedParamsObject();
301✔
794
        return getQueryParamString(queryParams);
795
    }
796

301✔
797
    protected getEmbedParamsObject() {
301✔
798
        const params = this.getBaseQueryParams();
301✔
799
        return params;
800
    }
801

802
    protected getRootIframeSrc() {
×
803
        const query = this.getEmbedParams();
×
804
        return this.getEmbedBasePath(query);
805
    }
806

807
    protected createIframeEl(frameSrc: string): HTMLIFrameElement {
×
808
        const iFrame = document.createElement('iframe');
×
809

810
        iFrame.src = frameSrc;
811
        iFrame.id = TS_EMBED_ID;
812
        iFrame.setAttribute('data-ts-iframe', 'true');
114✔
813

114✔
814
        // according to screenfull.js documentation
815
        // allowFullscreen, webkitallowfullscreen and mozallowfullscreen must be
816
        // true
817
        iFrame.allowFullscreen = true;
422✔
818
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
819
        // @ts-ignore
422✔
820
        iFrame.webkitallowfullscreen = true;
422✔
821
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
422✔
822
        // @ts-ignore
823
        iFrame.mozallowfullscreen = true;
824
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
825
        // @ts-ignore
826
        iFrame.allow = 'clipboard-read; clipboard-write; fullscreen;';
422✔
827

828
        const frameParams = this.viewConfig.frameParams;
829
        const { height: frameHeight, width: frameWidth, ...restParams } = frameParams || {};
422✔
830
        const width = getCssDimension(frameWidth || DEFAULT_EMBED_WIDTH);
831
        const height = getCssDimension(frameHeight || DEFAULT_EMBED_HEIGHT);
832
        setAttributes(iFrame, restParams);
422✔
833

834
        iFrame.style.width = `${width}`;
835
        iFrame.style.height = `${height}`;
422✔
836
        // Set minimum height to the frame so that,
837
        // scaling down on the fullheight doesn't make it too small.
422✔
838
        iFrame.style.minHeight = `${height}`;
422✔
839
        iFrame.style.border = '0';
422✔
840
        iFrame.name = 'ThoughtSpot Embedded Analytics';
422✔
841
        return iFrame;
422✔
842
    }
843

422✔
844
    protected handleInsertionIntoDOM(child: string | Node): void {
422✔
845
        if (this.isPreRendered) {
846
            this.insertIntoDOMForPreRender(child);
847
        } else {
422✔
848
            this.insertIntoDOM(child);
422✔
849
        }
422✔
850
        if (this.insertedDomEl instanceof Node) {
422✔
851
            (this.insertedDomEl as any)[this.embedNodeKey] = this;
852
        }
853
    }
854

439✔
855
    /**
17✔
856
     * Renders the embedded ThoughtSpot app in an iframe and sets up
857
     * event listeners.
422✔
858
     * @param url - The URL of the embedded ThoughtSpot app.
859
     */
439✔
860
    protected async renderIFrame(url: string): Promise<any> {
433✔
861
        if (this.isError) {
862
            return null;
863
        }
864
        if (!this.thoughtSpotHost) {
865
            this.throwInitError();
866
        }
867
        if (url.length > URL_MAX_LENGTH) {
868
            // warn: The URL is too long
869
        }
870

447✔
871
        return renderInQueue((nextInQueue) => {
8✔
872
            const initTimestamp = Date.now();
873

439✔
874
            this.executeCallbacks(EmbedEvent.Init, {
1✔
875
                data: {
876
                    timestamp: initTimestamp,
439!
877
                },
878
                type: EmbedEvent.Init,
879
            });
880

439✔
881
            uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_START);
439✔
882

883
            // Always subscribe to network events, regardless of auth status
439✔
884
            this.subscribeToNetworkEvents();
885

886
            return getAuthPromise()
887
                ?.then((isLoggedIn: boolean) => {
888
                    if (!isLoggedIn) {
889
                        this.handleInsertionIntoDOM(this.embedConfig.loginFailedMessage);
890
                        return;
439✔
891
                    }
892

893
                    this.setIframeElement(this.iFrame || this.createIframeEl(url));
439✔
894
                    this.iFrame.addEventListener('load', () => {
895
                        nextInQueue();
439!
896
                        const loadTimestamp = Date.now();
897
                        this.executeCallbacks(EmbedEvent.Load, {
434✔
898
                            data: {
3✔
899
                                timestamp: loadTimestamp,
3✔
900
                            },
901
                            type: EmbedEvent.Load,
902
                        });
431✔
903
                        uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_COMPLETE, {
431✔
904
                            elWidth: this.iFrame.clientWidth,
17✔
905
                            elHeight: this.iFrame.clientHeight,
17✔
906
                            timeTookToLoad: loadTimestamp - initTimestamp,
17✔
907
                        });
908
                        // Send info event  if preauth cache is enabled
909
                        if (this.isPreAuthCacheEnabled()) {
910
                            getPreauthInfo().then((data) => {
911
                                if (data?.info) {
912
                                    this.trigger(HostEvent.InfoSuccess, data);
17✔
913
                                }
914
                            });
915
                        }
916

917
                        // Setup fullscreen change handler after iframe is
918
                        // loaded and ready
17✔
919
                        this.setupFullscreenChangeHandler();
13✔
920
                    });
13✔
921
                    this.iFrame.addEventListener('error', () => {
5✔
922
                        nextInQueue();
923
                    });
924
                    this.handleInsertionIntoDOM(this.iFrame);
925
                    const prefetchIframe = document.querySelectorAll('.prefetchIframe');
926
                    if (prefetchIframe.length) {
927
                        prefetchIframe.forEach((el) => {
928
                            el.remove();
17✔
929
                        });
930
                    }
431✔
931
                    // Subscribe to message events only after successful
1✔
932
                    // auth and iframe setup
933
                    this.subscribeToMessageEvents();
431✔
934
                })
431✔
935
                .catch((error) => {
431!
936
                    nextInQueue();
×
937
                    uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_FAILED, {
×
938
                        error: JSON.stringify(error),
939
                    });
940
                    this.handleInsertionIntoDOM(this.embedConfig.loginFailedMessage);
941
                    this.handleError({
942
                        errorType: ErrorDetailsTypes.API,
431✔
943
                        message: error.message || ERROR_MESSAGE.LOGIN_FAILED,
944
                        code: EmbedErrorCodes.LOGIN_FAILED,
945
                        error : error,
5✔
946
                    });
5✔
947
                });
948
        });
949
    }
5✔
950

5✔
951
    protected createPreRenderWrapper(): HTMLDivElement {
952
        const preRenderIds = this.getPreRenderIds();
6✔
953

954
        document.getElementById(preRenderIds.wrapper)?.remove();
955

956
        const preRenderWrapper = document.createElement('div');
957
        preRenderWrapper.id = preRenderIds.wrapper;
958
        const initialPreRenderWrapperStyle = {
959
            position: 'absolute',
960
            width: '100vw',
961
            height: '100vh',
17✔
962
        };
963
        setStyleProperties(preRenderWrapper, initialPreRenderWrapperStyle);
17✔
964

965
        return preRenderWrapper;
17✔
966
    }
17✔
967

17✔
968
    protected preRenderWrapper: HTMLElement;
969

970
    protected preRenderChild: HTMLElement;
971

972
    protected connectPreRendered(): boolean {
17✔
973
        const preRenderIds = this.getPreRenderIds();
974
        const preRenderWrapperElement = document.getElementById(preRenderIds.wrapper);
17✔
975
        this.preRenderWrapper = this.preRenderWrapper || preRenderWrapperElement;
976

977
        this.preRenderChild = this.preRenderChild || document.getElementById(preRenderIds.child);
978

979
        if (this.preRenderWrapper && this.preRenderChild) {
980
            this.isPreRendered = true;
981
            if (this.preRenderChild instanceof HTMLIFrameElement) {
982
                this.setIframeElement(this.preRenderChild);
25✔
983
            }
25✔
984
            this.insertedDomEl = this.preRenderWrapper;
25✔
985
            this.isRendered = true;
986
        }
25✔
987

988
        return this.isPreRenderAvailable();
25✔
989
    }
8✔
990

8!
991
    protected isPreRenderAvailable(): boolean {
8✔
992
        return (
993
            this.isRendered
8✔
994
            && this.isPreRendered
8✔
995
            && Boolean(this.preRenderWrapper && this.preRenderChild)
996
        );
997
    }
25✔
998

999
    protected createPreRenderChild(child: string | Node): HTMLElement {
1000
        const preRenderIds = this.getPreRenderIds();
1001

61✔
1002
        document.getElementById(preRenderIds.child)?.remove();
131✔
1003

1004
        if (child instanceof HTMLElement) {
70✔
1005
            child.id = preRenderIds.child;
1006
            return child;
1007
        }
1008

1009
        const divChildNode = document.createElement('div');
17✔
1010
        setStyleProperties(divChildNode, { width: '100%', height: '100%' });
1011
        divChildNode.id = preRenderIds.child;
17✔
1012

1013
        if (typeof child === 'string') {
17✔
1014
            divChildNode.innerHTML = child;
16✔
1015
        } else {
16✔
1016
            divChildNode.appendChild(child);
1017
        }
1018

1✔
1019
        return divChildNode;
1✔
1020
    }
1✔
1021

1022
    protected insertIntoDOMForPreRender(child: string | Node): void {
1!
1023
        const preRenderChild = this.createPreRenderChild(child);
1✔
1024
        const preRenderWrapper = this.createPreRenderWrapper();
1025
        preRenderWrapper.appendChild(preRenderChild);
×
1026

1027
        this.preRenderChild = preRenderChild;
1028
        this.preRenderWrapper = preRenderWrapper;
1✔
1029

1030
        if (preRenderChild instanceof HTMLIFrameElement) {
1031
            this.setIframeElement(preRenderChild);
1032
        }
17✔
1033
        this.insertedDomEl = preRenderWrapper;
17✔
1034

17✔
1035
        if (this.showPreRenderByDefault) {
1036
            this.showPreRender();
17✔
1037
        } else {
17✔
1038
            this.hidePreRender();
1039
        }
17✔
1040

16✔
1041
        document.body.appendChild(preRenderWrapper);
1042
    }
17✔
1043

1044
    private showPreRenderByDefault = false;
17✔
1045

2✔
1046
    protected insertIntoDOM(child: string | Node): void {
1047
        if (this.viewConfig.insertAsSibling) {
15✔
1048
            if (typeof child === 'string') {
1049
                const div = document.createElement('div');
1050
                div.innerHTML = child;
17✔
1051
                div.id = TS_EMBED_ID;
1052

1053
                child = div;
490✔
1054
            }
1055
            if (this.el.nextElementSibling?.id === TS_EMBED_ID) {
1056
                this.el.nextElementSibling.remove();
422✔
1057
            }
12✔
1058
            this.el.parentElement.insertBefore(child, this.el.nextSibling);
1✔
1059
            this.insertedDomEl = child;
1✔
1060
        } else if (typeof child === 'string') {
1✔
1061
            this.el.innerHTML = child;
1062
            this.insertedDomEl = this.el.children[0];
1✔
1063
        } else {
1064
            this.el.innerHTML = '';
12✔
1065
            this.el.appendChild(child);
1✔
1066
            this.insertedDomEl = child;
1067
        }
12✔
1068
    }
12✔
1069

410✔
1070
    /**
6✔
1071
     * Sets the height of the iframe
6✔
1072
     * @param height The height in pixels
1073
     */
404✔
1074
    protected setIFrameHeight(height: number | string): void {
404✔
1075
        this.iFrame.style.height = getCssDimension(height);
404✔
1076
    }
1077

1078
    /**
1079
     * We can process the customer given payload before sending it to the embed port
1080
     * Embed event handler -> responder -> createEmbedEventResponder -> send response
1081
     * @param eventPort The event port for a specific MessageChannel
1082
     * @param eventType The event type
1083
     * @returns 
1084
     */
3✔
1085
    protected createEmbedEventResponder = (eventPort: MessagePort | void, eventType: EmbedEvent) => {
1086

1087
        const getPayloadToSend = (payload: any) => {
1088
            if (eventType === EmbedEvent.OnBeforeGetVizDataIntercept) {
1089
                return processLegacyInterceptResponse(payload);
1090
            }
1091
            if (eventType === EmbedEvent.ApiIntercept) {
1092
                return processApiInterceptResponse(payload);
1093
            }
1094
            return payload;
490✔
1095
        }   
1096
        return (payload: any) => {
64✔
1097
            const payloadToSend = getPayloadToSend(payload);
29✔
1098
            this.triggerEventOnPort(eventPort, payloadToSend);
1✔
1099
        }
1100
    }
28!
1101

×
1102
    /**
1103
     * Executes all registered event handlers for a particular event type
28✔
1104
     * @param eventType The event type
1105
     * @param data The payload invoked with the event handler
64✔
1106
     * @param eventPort The event Port for a specific MessageChannel
29✔
1107
     */
29✔
1108
    protected executeCallbacks(
1109
        eventType: EmbedEvent,
1110
        data: any,
1111
        eventPort?: MessagePort | void,
1112
    ): void {
1113
        const eventHandlers = this.eventHandlerMap.get(eventType) || [];
1114
        const allHandlers = this.eventHandlerMap.get(EmbedEvent.ALL) || [];
1115
        const callbacks = [...eventHandlers, ...allHandlers];
1116
        const dataStatus = data?.status || embedEventStatus.END;
1117
        callbacks.forEach((callbackObj) => {
1118
            if (
1119
                // When start status is true it trigger only start releated
1120
                // payload
1121
                (callbackObj.options.start && dataStatus === embedEventStatus.START)
1122
                // When start status is false it trigger only end releated
539✔
1123
                // payload
539✔
1124
                || (!callbackObj.options.start && dataStatus === embedEventStatus.END)
539✔
1125
            ) {
539!
1126
                const responder = this.createEmbedEventResponder(eventPort, eventType);
539✔
1127
                callbackObj.callback(data, responder);
64✔
1128
            }
1129
        });
1130
    }
191✔
1131

1132
    /**
1133
     * Returns the ThoughtSpot hostname or IP address.
1134
     */
1135
    protected getThoughtSpotHost(): string {
63✔
1136
        return this.thoughtSpotHost;
63✔
1137
    }
1138

1139
    /**
1140
     * Gets the v1 event type (if applicable) for the EmbedEvent type
1141
     * @param eventType The v2 event type
1142
     * @returns The corresponding v1 event type if one exists
1143
     * or else the v2 event type itself
1144
     */
1145
    protected getCompatibleEventType(eventType: EmbedEvent): EmbedEvent {
×
1146
        return V1EventMap[eventType] || eventType;
1147
    }
1148

1149
    /**
1150
     * Calculates the iframe center for the current visible viewPort
1151
     * of iframe using Scroll position of Host App, offsetTop for iframe
1152
     * in Host app. ViewPort height of the tab.
1153
     * @returns iframe Center in visible viewport,
1154
     *  Iframe height,
1155
     *  View port height.
1,720✔
1156
     */
1157
    protected getIframeCenter() {
1158
        const offsetTopClient = getOffsetTop(this.iFrame);
1159
        const scrollTopClient = window.scrollY;
1160
        const viewPortHeight = window.innerHeight;
1161
        const iframeHeight = this.iFrame.offsetHeight;
1162
        const iframeScrolled = scrollTopClient - offsetTopClient;
1163
        let iframeVisibleViewPort;
1164
        let iframeOffset;
1165

1166
        if (iframeScrolled < 0) {
1167
            iframeVisibleViewPort = viewPortHeight - (offsetTopClient - scrollTopClient);
5✔
1168
            iframeVisibleViewPort = Math.min(iframeHeight, iframeVisibleViewPort);
5✔
1169
            iframeOffset = 0;
5✔
1170
        } else {
5✔
1171
            iframeVisibleViewPort = Math.min(iframeHeight - iframeScrolled, viewPortHeight);
5✔
1172
            iframeOffset = iframeScrolled;
1173
        }
1174
        const iframeCenter = iframeOffset + iframeVisibleViewPort / 2;
1175
        return {
5!
1176
            iframeCenter,
×
1177
            iframeScrolled,
×
1178
            iframeHeight,
×
1179
            viewPortHeight,
1180
            iframeVisibleViewPort,
5✔
1181
        };
5✔
1182
    }
1183

5✔
1184
    /**
5✔
1185
     * Registers an event listener to trigger an alert when the ThoughtSpot app
1186
     * sends an event of a particular message type to the host application.
1187
     * @param messageType The message type
1188
     * @param callback A callback as a function
1189
     * @param options The message options
1190
     * @param isSelf
1191
     * @param isRegisteredBySDK
1192
     * @example
1193
     * ```js
1194
     * tsEmbed.on(EmbedEvent.Error, (data) => {
1195
     *   console.error(data);
1196
     * });
1197
     * ```
1198
     * @example
1199
     * ```js
1200
     * tsEmbed.on(EmbedEvent.Save, (data) => {
1201
     *   console.log("Answer save clicked", data);
1202
     * }, {
1203
     *   start: true // This will trigger the callback on start of save
1204
     * });
1205
     * ```
1206
     */
1207
    public on(
1208
        messageType: EmbedEvent,
1209
        callback: MessageCallback,
1210
        options: MessageOptions = { start: false },
1211
        isRegisteredBySDK = false,
1212
    ): typeof TsEmbed.prototype {
1213
        uploadMixpanelEvent(`${MIXPANEL_EVENT.VISUAL_SDK_ON}-${messageType}`, {
1214
            isRegisteredBySDK,
1215
        });
1216
        if (this.isRendered) {
1217
            logger.warn('Please register event handlers before calling render');
1218
        }
1219
        const callbacks = this.eventHandlerMap.get(messageType) || [];
17✔
1220
        callbacks.push({ options, callback });
1,739✔
1221
        this.eventHandlerMap.set(messageType, callbacks);
1222
        return this;
2,584✔
1223
    }
1224

1225
    /**
2,584✔
1226
     * Removes an event listener for a particular event type.
2✔
1227
     * @param messageType The message type
1228
     * @param callback The callback to remove
2,584✔
1229
     * @example
2,584✔
1230
     * ```js
2,584✔
1231
     * const errorHandler = (data) => { console.error(data); };
2,584✔
1232
     * tsEmbed.on(EmbedEvent.Error, errorHandler);
1233
     * tsEmbed.off(EmbedEvent.Error, errorHandler);
1234
     * ```
1235
     */
1236
    public off(messageType: EmbedEvent, callback: MessageCallback): typeof TsEmbed.prototype {
1237
        const callbacks = this.eventHandlerMap.get(messageType) || [];
1238
        const index = callbacks.findIndex((cb) => cb.callback === callback);
1239
        if (index > -1) {
1240
            callbacks.splice(index, 1);
1241
        }
1242
        return this;
1243
    }
1244

1245
    /**
1246
     * Triggers an event on specific Port registered against
1!
1247
     * for the EmbedEvent
1✔
1248
     * @param eventType The message type
1!
1249
     * @param data The payload to send
1✔
1250
     * @param eventPort
1251
     * @param payload
1✔
1252
     */
1253
    private triggerEventOnPort(eventPort: MessagePort | void, payload: any) {
1254
        if (eventPort) {
1255
            try {
1256
                eventPort.postMessage({
1257
                    type: payload.type,
1258
                    data: payload.data,
1259
                });
1260
            } catch (e) {
1261
                eventPort.postMessage({ error: e });
1262
                logger.log(e);
1263
            }
29✔
1264
        } else {
25✔
1265
            logger.log('Event Port is not defined');
25✔
1266
        }
1267
    }
1268

1269
    /**
1270
    * @hidden
×
1271
    * Internal state to track if the embed container is loaded.
×
1272
    * This is used to trigger events after the embed container is loaded.
1273
    */
1274
    public isEmbedContainerLoaded = false;
4✔
1275

1276
    /**
1277
     * @hidden
1278
     * Internal state to track the callbacks to be executed after the embed container 
1279
     * is loaded.
1280
     * This is used to trigger events after the embed container is loaded.
1281
     */
1282
    private embedContainerReadyCallbacks: Array<() => void> = [];
1283

490✔
1284
    protected getPreRenderObj<T extends TsEmbed>(): T {
1285
        const embedObj = (this.insertedDomEl as any)?.[this.embedNodeKey] as T;
1286
        if (embedObj === (this as any)) {
1287
            logger.info('embedObj is same as this');
1288
        }
1289
        return embedObj;
1290
    }
1291

490✔
1292
    private checkEmbedContainerLoaded() {
1293
        if (this.isEmbedContainerLoaded) return true;
1294

34✔
1295
        const preRenderObj = this.getPreRenderObj<TsEmbed>();
34✔
1296
        if (preRenderObj && preRenderObj.isEmbedContainerLoaded) {
5✔
1297
            this.isEmbedContainerLoaded = true;
1298
        }
34✔
1299

1300
        return this.isEmbedContainerLoaded;
1301
    }
1302
    private executeEmbedContainerReadyCallbacks() {
32✔
1303
        logger.debug('executePendingEvents', this.embedContainerReadyCallbacks);
1304
        this.embedContainerReadyCallbacks.forEach((callback) => {
24✔
1305
            callback?.();
24✔
1306
        });
4✔
1307
        this.embedContainerReadyCallbacks = [];
1308
    }
1309

24✔
1310
    /**
1311
     * Executes a callback after the embed container is loaded.
1312
     * @param callback The callback to execute
14✔
1313
     */
14✔
1314
    protected executeAfterEmbedContainerLoaded(callback: () => void) {
12!
1315
        if (this.checkEmbedContainerLoaded()) {
1316
            callback?.();
14✔
1317
        } else {
1318
            logger.debug('pushing callback to embedContainerReadyCallbacks', callback);
1319
            this.embedContainerReadyCallbacks.push(callback);
1320
        }
1321
    }
1322

1323
    protected createEmbedContainerHandler = (source: EmbedEvent.AuthInit | EmbedEvent.EmbedListenerReady) => () => {
1324
        const processEmbedContainerReady = () => {
31✔
1325
            logger.debug('processEmbedContainerReady');
11!
1326
            this.isEmbedContainerLoaded = true;
1327
            this.executeEmbedContainerReadyCallbacks();
20✔
1328
        }
20✔
1329
        if (source === EmbedEvent.AuthInit) {
1330
            const AUTH_INIT_FALLBACK_DELAY = 1000;
1331
            // Wait for 1 second to ensure the embed container is loaded
1332
            // This is a workaround to ensure the embed container is loaded
982✔
1333
            // this is needed until all clusters have EmbedListenerReady event
10✔
1334
            setTimeout(processEmbedContainerReady, AUTH_INIT_FALLBACK_DELAY);
9✔
1335
        } else if (source === EmbedEvent.EmbedListenerReady) {
9✔
1336
            processEmbedContainerReady();
9✔
1337
        }
1338
    }
10✔
1339

7✔
1340
    /**
1341
     * Triggers an event to the embedded app
1342
     * @param {HostEvent} messageType The event type
1343
     * @param {any} data The payload to send with the message
7✔
1344
     * @returns A promise that resolves with the response from the embedded app
3!
1345
     */
3✔
1346
    public async trigger<HostEventT extends HostEvent, PayloadT>(
1347
        messageType: HostEventT,
1348
        data: TriggerPayload<PayloadT, HostEventT> = {} as any,
1349
    ): Promise<TriggerResponse<PayloadT, HostEventT>> {
1350
        uploadMixpanelEvent(`${MIXPANEL_EVENT.VISUAL_SDK_TRIGGER}-${messageType}`);
1351

1352
        if (!this.isRendered) {
1353
            this.handleError({
1354
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
1355
                message: ERROR_MESSAGE.RENDER_BEFORE_EVENTS_REQUIRED,
1356
                code: EmbedErrorCodes.RENDER_NOT_CALLED,
1357
                error: ERROR_MESSAGE.RENDER_BEFORE_EVENTS_REQUIRED,
222✔
1358
            });
1359
            return null;
322✔
1360
        }
1361

322!
1362
        if (!messageType) {
×
1363
            this.handleError({
1364
                errorType: ErrorDetailsTypes.VALIDATION_ERROR,
1365
                message: ERROR_MESSAGE.HOST_EVENT_TYPE_UNDEFINED,
1366
                code: EmbedErrorCodes.HOST_EVENT_TYPE_UNDEFINED,
1367
                error: ERROR_MESSAGE.HOST_EVENT_TYPE_UNDEFINED,
1368
            });
×
1369
            return null;
1370
        }
1371

322✔
1372
        // Check if iframe exists before triggering - 
1✔
1373
        // this prevents the error when auth fails
1374
        if (!this.iFrame) {
1375
            logger.debug(
1376
                `Cannot trigger ${messageType} - iframe not available (likely due to auth failure)`,
1377
            );
1378
            return null;
1✔
1379
        }
1380

1381
        // send an empty object, this is needed for liveboard default handlers
1382
        return this.hostEventClient.triggerHostEvent(messageType, data);
1383
    }
321✔
1384

22✔
1385
    /**
1386
     * Triggers an event to the embedded app, skipping the UI flow.
1387
     * @param {UIPassthroughEvent} apiName - The name of the API to be triggered.
22✔
1388
     * @param {UIPassthroughRequest} parameters - The parameters to be passed to the API.
1389
     * @returns {Promise<UIPassthroughRequest>} - A promise that resolves with the response
1390
     * from the embedded app.
1391
     */
299✔
1392
    public async triggerUIPassThrough<UIPassthroughEventT extends UIPassthroughEvent>(
1393
        apiName: UIPassthroughEventT,
1394
        parameters: UIPassthroughRequest<UIPassthroughEventT>,
1395
    ): Promise<UIPassthroughArrayResponse<UIPassthroughEventT>> {
1396
        const response = this.hostEventClient.triggerUIPassthroughApi(apiName, parameters);
1397
        return response;
1398
    }
1399

1400
    /**
1401
     * Marks the ThoughtSpot object to have been rendered
1402
     * Needs to be overridden by subclasses to do the actual
1403
     * rendering of the iframe.
1404
     * @param args
1405
     */
3✔
1406
    public async render(): Promise<TsEmbed> {
3✔
1407
        if (!getIsInitCalled()) {
1408
            logger.error(ERROR_MESSAGE.RENDER_CALLED_BEFORE_INIT);
1409
        }
1410
        await this.isReadyForRenderPromise;
1411
        this.isRendered = true;
1412

1413
        return this;
1414
    }
1415

1416
    public getIframeSrc(): string {
441✔
1417
        return '';
2✔
1418
    }
1419

441✔
1420
    protected handleRenderForPrerender() {
441✔
1421
        return this.render();
1422
    }
441✔
1423

1424
    /**
1425
     * Creates the preRender shell
1426
     * @param showPreRenderByDefault - Show the preRender after render, hidden by default
1✔
1427
     */
1428

1429
    public async preRender(showPreRenderByDefault = false, replaceExistingPreRender = false): Promise<TsEmbed> {
1430
        if (!this.viewConfig.preRenderId) {
12✔
1431
            logger.error(ERROR_MESSAGE.PRERENDER_ID_MISSING);
1432
            return this;
1433
        }
1434
        this.isPreRendered = true;
1435
        this.showPreRenderByDefault = showPreRenderByDefault;
1436

1437
        const isAlreadyRendered = this.connectPreRendered();
1438
        if (isAlreadyRendered && !replaceExistingPreRender) {
30✔
1439
            return this;
19✔
1440
        }
1✔
1441

1✔
1442
        return this.handleRenderForPrerender();
1443
    }
18✔
1444

18✔
1445
    /**
1446
     * Get the Post Url Params for THOUGHTSPOT from the current
18✔
1447
     * host app URL.
18✔
1448
     * THOUGHTSPOT URL params starts with a prefix "ts-"
1✔
1449
     * @version SDK: 1.14.0 | ThoughtSpot: 8.4.0.cl, 8.4.1-sw
1450
     */
1451
    public getThoughtSpotPostUrlParams(
17✔
1452
        additionalParams: { [key: string]: string | number } = {},
1453
    ): string {
1454
        const urlHash = window.location.hash;
1455
        const queryParams = window.location.search;
1456
        const postHashParams = urlHash.split('?');
1457
        const postURLParams = postHashParams[postHashParams.length - 1];
1458
        const queryParamsObj = new URLSearchParams(queryParams);
1459
        const postURLParamsObj = new URLSearchParams(postURLParams);
1460
        const params = new URLSearchParams();
1461

424✔
1462
        const addKeyValuePairCb = (value: string, key: string): void => {
1463
            if (key.startsWith(THOUGHTSPOT_PARAM_PREFIX)) {
448✔
1464
                params.append(key, value);
448✔
1465
            }
448✔
1466
        };
448✔
1467
        queryParamsObj.forEach(addKeyValuePairCb);
448✔
1468
        postURLParamsObj.forEach(addKeyValuePairCb);
448✔
1469
        Object.entries(additionalParams).forEach(([k, v]) => params.append(k, v as string));
448✔
1470

1471
        let tsParams = params.toString();
448✔
1472
        tsParams = tsParams ? `?${tsParams}` : '';
8✔
1473

5✔
1474
        return tsParams;
1475
    }
1476

448✔
1477
    /**
448✔
1478
     * Destroys the ThoughtSpot embed, and remove any nodes from the DOM.
448✔
1479
     * @version SDK: 1.19.1 | ThoughtSpot: *
1480
     */
448✔
1481
    public destroy(): void {
448✔
1482
        try {
1483
            this.removeFullscreenChangeHandler();
448✔
1484
            this.unsubscribeToEvents();
1485
            if (!getEmbedConfig().waitForCleanupOnDestroy) {
1486
                this.trigger(HostEvent.DestroyEmbed)
1487
                this.insertedDomEl?.parentNode?.removeChild(this.insertedDomEl);
1488
            } else {
1489
                const cleanupTimeout = getEmbedConfig().cleanupTimeout;
1490
                Promise.race([
1491
                    this.trigger(HostEvent.DestroyEmbed),
31✔
1492
                    new Promise((resolve) => setTimeout(resolve, cleanupTimeout)),
31✔
1493
                ]).then(() => {
31✔
1494
                    this.insertedDomEl?.parentNode?.removeChild(this.insertedDomEl);
31✔
1495
                }).catch((e) => {
28✔
1496
                    logger.log('Error destroying TS Embed', e);
28!
1497
                });
1498
            }
3✔
1499
        } catch (e) {
3✔
1500
            logger.log('Error destroying TS Embed', e);
1501
        }
3✔
1502
    }
1503

3!
1504
    public getUnderlyingFrameElement(): HTMLIFrameElement {
1505
        return this.iFrame;
×
1506
    }
1507

1508
    /**
1509
     * Prerenders a generic instance of the TS component.
1✔
1510
     * This means without the path but with the flags already applied.
1511
     * This is useful for prerendering the component in the background.
1512
     * @version SDK: 1.22.0
1513
     * @returns
1514
     */
1✔
1515
    public async prerenderGeneric(): Promise<any> {
1516
        if (!getIsInitCalled()) {
1517
            logger.error(ERROR_MESSAGE.RENDER_CALLED_BEFORE_INIT);
1518
        }
1519
        await this.isReadyForRenderPromise;
1520

1521
        const prerenderFrameSrc = this.getRootIframeSrc();
1522
        this.isRendered = true;
1523
        return this.renderIFrame(prerenderFrameSrc);
1524
    }
1525

8✔
1526
    protected beforePrerenderVisible(): void {
1✔
1527
        // Override in subclass
1528
    }
8✔
1529

1530
    private validatePreRenderViewConfig = (viewConfig: ViewConfig) => {
8✔
1531
        const preRenderAllowedKeys = ['preRenderId', 'vizId', 'liveboardId'];
8✔
1532
        const preRenderedObject = (this.insertedDomEl as any)?.[this.embedNodeKey] as TsEmbed;
8✔
1533
        if (!preRenderedObject) return;
1534
        if (viewConfig.preRenderId) {
1535
            const allOtherKeys = Object.keys(viewConfig).filter(
1536
                (key) => !preRenderAllowedKeys.includes(key) && !key.startsWith('on'),
1537
            );
1538

1539
            allOtherKeys.forEach((key: keyof ViewConfig) => {
490✔
1540
                if (
5✔
1541
                    !isUndefined(viewConfig[key])
5!
1542
                    && !isEqual(viewConfig[key], preRenderedObject.viewConfig[key])
5!
1543
                ) {
5!
1544
                    logger.warn(
5✔
1545
                        `${viewConfig.embedComponentType || 'Component'} was pre-rendered with `
29✔
1546
                        + `"${key}" as "${JSON.stringify(preRenderedObject.viewConfig[key])}" `
1547
                        + `but a different value "${JSON.stringify(viewConfig[key])}" `
1548
                        + 'was passed to the Embed component. '
5✔
1549
                        + 'The new value provided is ignored, the value provided during '
17✔
1550
                        + 'preRender is used.',
34✔
1551
                    );
1552
                }
1553
            });
7✔
1554
        }
7!
1555
    };
1556

1557
    /**
1558
     * Displays the PreRender component.
1559
     * If the component is not preRendered, it attempts to create and render it.
1560
     * Also, synchronizes the style of the PreRender component with the embedding
1561
     * element.
1562
     */
1563
    public async showPreRender(): Promise<TsEmbed> {
1564
        if (!this.viewConfig.preRenderId) {
1565
            logger.error(ERROR_MESSAGE.PRERENDER_ID_MISSING);
1566
            return this;
1567
        }
1568
        if (!this.isPreRenderAvailable()) {
1569
            const isAvailable = this.connectPreRendered();
1570

1571
            if (!isAvailable) {
1572
                // if the Embed component is not preRendered , Render it now and
1573
                return this.preRender(true);
11✔
1574
            }
1✔
1575
            this.validatePreRenderViewConfig(this.viewConfig);
1✔
1576
            logger.debug('triggering UpdateEmbedParams', this.viewConfig);
1577
            this.executeAfterEmbedContainerLoaded(() => {
10✔
1578
                this.trigger(HostEvent.UpdateEmbedParams, this.getUpdateEmbedParamsObject());
7✔
1579
            });
1580
        }
7✔
1581

1582
        this.beforePrerenderVisible();
2✔
1583

1584
        if (this.el) {
5✔
1585
            this.syncPreRenderStyle();
5✔
1586
            if (!this.viewConfig.doNotTrackPreRenderSize) {
5✔
1587
                this.resizeObserver = new ResizeObserver((entries) => {
4✔
1588
                    entries.forEach((entry) => {
4✔
1589
                        if (entry.contentRect && entry.target === this.el) {
1590
                            setStyleProperties(this.preRenderWrapper, {
1591
                                width: `${entry.contentRect.width}px`,
1592
                                height: `${entry.contentRect.height}px`,
1593
                            });
8✔
1594
                        }
1595
                    });
8!
1596
                });
8✔
1597
                this.resizeObserver.observe(this.el);
8!
1598
            }
8✔
1599
        }
1✔
1600

1!
1601
        removeStyleProperties(this.preRenderWrapper, ['z-index', 'opacity', 'pointer-events']);
1✔
1602

1603
        this.subscribeToEvents();
1604

1605
        // Setup fullscreen change handler for prerendered components
1606
        if (this.iFrame) {
1607
            this.setupFullscreenChangeHandler();
1608
        }
8✔
1609

1610
        return this;
1611
    }
1612

8✔
1613
    /**
1614
     * Synchronizes the style properties of the PreRender component with the embedding
8✔
1615
     * element. This function adjusts the position, width, and height of the PreRender
1616
     * component
1617
     * to match the dimensions and position of the embedding element.
8!
1618
     * @throws {Error} Throws an error if the embedding element (passed as domSelector)
8✔
1619
     * is not defined or not found.
1620
     */
1621
    public syncPreRenderStyle(): void {
8✔
1622
        if (!this.isPreRenderAvailable() || !this.el) {
1623
            logger.error(ERROR_MESSAGE.SYNC_STYLE_CALLED_BEFORE_RENDER);
1624
            return;
1625
        }
1626
        const elBoundingClient = this.el.getBoundingClientRect();
1627

1628
        setStyleProperties(this.preRenderWrapper, {
1629
            top: `${elBoundingClient.y + window.scrollY}px`,
1630
            left: `${elBoundingClient.x + window.scrollX}px`,
1631
            width: `${elBoundingClient.width}px`,
1632
            height: `${elBoundingClient.height}px`,
1633
        });
9✔
1634
    }
1✔
1635

1✔
1636
    /**
1637
     * Hides the PreRender component if it is available.
8✔
1638
     * If the component is not preRendered, it issues a warning.
1639
     */
8✔
1640
    public hidePreRender(): void {
1641
        if (!this.isPreRenderAvailable()) {
1642
            // if the embed component is not preRendered , nothing to hide
1643
            logger.warn('PreRender should be called before hiding it using hidePreRender.');
1644
            return;
1645
        }
1646
        const preRenderHideStyles = {
1647
            opacity: '0',
1648
            pointerEvents: 'none',
1649
            zIndex: '-1000',
1650
            position: 'absolute ',
1651
        };
1652
        setStyleProperties(this.preRenderWrapper, preRenderHideStyles);
17✔
1653

1654
        if (this.resizeObserver) {
1✔
1655
            this.resizeObserver.disconnect();
1✔
1656
        }
1657

16✔
1658
        this.unsubscribeToEvents();
1659
    }
1660

1661
    /**
1662
     * Retrieves unique HTML element IDs for PreRender-related elements.
1663
     * These IDs are constructed based on the provided 'preRenderId' from 'viewConfig'.
16✔
1664
     * @returns {object} An object containing the IDs for the PreRender elements.
1665
     * @property {string} wrapper - The HTML element ID for the PreRender wrapper.
16✔
1666
     * @property {string} child - The HTML element ID for the PreRender child.
1✔
1667
     */
1668
    public getPreRenderIds() {
1669
        return {
16✔
1670
            wrapper: `tsEmbed-pre-render-wrapper-${this.viewConfig.preRenderId}`,
1671
            child: `tsEmbed-pre-render-child-${this.viewConfig.preRenderId}`,
1672
        };
1673
    }
1674

1675
    /**
1676
     * Returns the answerService which can be used to make arbitrary graphql calls on top
1677
     * session.
1678
     * @param vizId [Optional] to get for a specific viz in case of a Liveboard.
1679
     * @version SDK: 1.25.0 / ThoughtSpot 9.10.0
1680
     */
69✔
1681
    public async getAnswerService(vizId?: string): Promise<AnswerService> {
1682
        const { session } = await this.trigger(HostEvent.GetAnswerSession, vizId ? { vizId } : {});
1683
        return new AnswerService(session, null, this.embedConfig.thoughtSpotHost);
1684
    }
1685

1686
    /**
1687
     * Set up fullscreen change detection to automatically trigger ExitPresentMode
1688
     * when user exits fullscreen mode
1689
     */
1690
    private setupFullscreenChangeHandler() {
1691
        const embedConfig = getEmbedConfig();
1692
        const disableFullscreenPresentation = embedConfig?.disableFullscreenPresentation ?? true;
1693

1!
1694
        if (disableFullscreenPresentation) {
1✔
1695
            return;
1696
        }
1697

1698
        if (this.fullscreenChangeHandler) {
1699
            document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
1700
        }
1701

1702
        this.fullscreenChangeHandler = () => {
29✔
1703
            const isFullscreen = !!document.fullscreenElement;
29!
1704
            if (!isFullscreen) {
1705
                logger.info('Exited fullscreen mode - triggering ExitPresentMode');
29✔
1706
                // Only trigger if iframe is available and contentWindow is
26✔
1707
                // accessible
1708
                if (this.iFrame && this.iFrame.contentWindow) {
1709
                    this.trigger(HostEvent.ExitPresentMode);
3!
1710
                } else {
×
1711
                    logger.debug('Skipping ExitPresentMode - iframe contentWindow not available');
1712
                }
1713
            }
3✔
1714
        };
5✔
1715

5✔
1716
        document.addEventListener('fullscreenchange', this.fullscreenChangeHandler);
2✔
1717
    }
1718

1719
    /**
2✔
1720
     * Remove fullscreen change handler
1✔
1721
     */
1722
    private removeFullscreenChangeHandler() {
1✔
1723
        if (this.fullscreenChangeHandler) {
1724
            document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
1725
            this.fullscreenChangeHandler = null;
1726
        }
1727
    }
3✔
1728
}
1729

1730
/**
1731
 * Base class for embedding v1 experience
1732
 * Note: The v1 version of ThoughtSpot Blink works on the AngularJS stack
1733
 * which is currently under migration to v2
1734
 * @inheritdoc
32!
1735
 */
×
1736
export class V1Embed extends TsEmbed {
×
1737
    protected viewConfig: ViewConfig;
1738

1739
    constructor(domSelector: DOMSelector, viewConfig: ViewConfig) {
1740
        super(domSelector, viewConfig);
1741
        this.viewConfig = { excludeRuntimeFiltersfromURL: false, ...viewConfig };
1742
    }
1743

1744
    /**
1745
     * Render the app in an iframe and set up event handlers
1746
     * @param iframeSrc
1747
     */
14✔
1748
    protected renderV1Embed(iframeSrc: string): Promise<any> {
1749
        return this.renderIFrame(iframeSrc);
1750
    }
1751

321✔
1752
    protected getRootIframeSrc(): string {
321✔
1753
        const queryParams = this.getEmbedParams();
1754
        let queryString = queryParams;
1755

1756
        if (!this.viewConfig.excludeRuntimeParametersfromURL) {
1757
            const runtimeParameters = this.viewConfig.runtimeParameters;
1758
            const parameterQuery = getRuntimeParameters(runtimeParameters || []);
1759
            queryString = [parameterQuery, queryParams].filter(Boolean).join('&');
1760
        }
293✔
1761

1762
        if (!this.viewConfig.excludeRuntimeFiltersfromURL) {
1763
            const runtimeFilters = this.viewConfig.runtimeFilters;
1764

302✔
1765
            const filterQuery = getFilterQuery(runtimeFilters || []);
302✔
1766
            queryString = [filterQuery, queryString].filter(Boolean).join('&');
1767
        }
302✔
1768
        return this.viewConfig.enableV2Shell_experimental
301✔
1769
            ? this.getEmbedBasePath(queryString)
301✔
1770
            : this.getV1EmbedBasePath(queryString);
301✔
1771
    }
1772

1773
    /**
302✔
1774
     * @inheritdoc
300✔
1775
     * @example
1776
     * ```js
300✔
1777
     * tsEmbed.on(EmbedEvent.Error, (data) => {
300✔
1778
     *   console.error(data);
1779
     * });
302✔
1780
     * ```
1781
     * @example
1782
     * ```js
1783
     * tsEmbed.on(EmbedEvent.Save, (data) => {
1784
     *   console.log("Answer save clicked", data);
1785
     * }, {
1786
     *   start: true // This will trigger the callback on start of save
1787
     * });
1788
     * ```
1789
     */
1790
    public on(
1791
        messageType: EmbedEvent,
1792
        callback: MessageCallback,
1793
        options: MessageOptions = { start: false },
1794
    ): typeof TsEmbed.prototype {
1795
        const eventType = this.getCompatibleEventType(messageType);
1796
        return super.on(eventType, callback, options);
1797
    }
1798

1799
    /**
1800
     * Only for testing purposes.
1801
     * @hidden
1802
     */
1803

1804
    public test__executeCallbacks = this.executeCallbacks;
150✔
1805
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc