@thoughtspot/visual-embed-sdk 1.39.3 → 1.40.1-alpha.1

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.
Files changed (93) hide show
  1. package/cjs/src/embed/app.d.ts +56 -0
  2. package/cjs/src/embed/app.d.ts.map +1 -1
  3. package/cjs/src/embed/app.js +47 -8
  4. package/cjs/src/embed/app.js.map +1 -1
  5. package/cjs/src/embed/app.spec.js +322 -7
  6. package/cjs/src/embed/app.spec.js.map +1 -1
  7. package/cjs/src/embed/liveboard.d.ts +58 -1
  8. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  9. package/cjs/src/embed/liveboard.js +59 -8
  10. package/cjs/src/embed/liveboard.js.map +1 -1
  11. package/cjs/src/embed/liveboard.spec.js +206 -0
  12. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  13. package/cjs/src/embed/ts-embed.d.ts +7 -0
  14. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  15. package/cjs/src/embed/ts-embed.js +61 -7
  16. package/cjs/src/embed/ts-embed.js.map +1 -1
  17. package/cjs/src/types.d.ts +37 -6
  18. package/cjs/src/types.d.ts.map +1 -1
  19. package/cjs/src/types.js +35 -4
  20. package/cjs/src/types.js.map +1 -1
  21. package/cjs/src/utils/processTrigger.js +2 -1
  22. package/cjs/src/utils/processTrigger.js.map +1 -1
  23. package/cjs/src/utils.d.ts +6 -0
  24. package/cjs/src/utils.d.ts.map +1 -1
  25. package/cjs/src/utils.js +23 -3
  26. package/cjs/src/utils.js.map +1 -1
  27. package/cjs/src/utils.spec.js +212 -1
  28. package/cjs/src/utils.spec.js.map +1 -1
  29. package/dist/{index-ZrE8YYq8.js → index-CmEQfuE3.js} +1 -1
  30. package/dist/index-D1pyb7RG.js +7371 -0
  31. package/dist/index-DeFzsyFF.js +7371 -0
  32. package/dist/index-Dpf0rd6w.js +7371 -0
  33. package/dist/index-UuEbsISo.js +7447 -0
  34. package/dist/index-e3Uw3YFO.js +7371 -0
  35. package/dist/src/embed/app.d.ts +56 -0
  36. package/dist/src/embed/app.d.ts.map +1 -1
  37. package/dist/src/embed/bodyless-conversation.d.ts +0 -4
  38. package/dist/src/embed/bodyless-conversation.d.ts.map +1 -1
  39. package/dist/src/embed/liveboard.d.ts +56 -0
  40. package/dist/src/embed/liveboard.d.ts.map +1 -1
  41. package/dist/src/react/index.d.ts +0 -2
  42. package/dist/src/react/index.d.ts.map +1 -1
  43. package/dist/src/types.d.ts +16 -198
  44. package/dist/src/types.d.ts.map +1 -1
  45. package/dist/src/utils/graphql/nlsService/conversation-service.d.ts.map +1 -1
  46. package/dist/src/utils.d.ts +6 -0
  47. package/dist/src/utils.d.ts.map +1 -1
  48. package/dist/tsembed-react.es.js +137 -224
  49. package/dist/tsembed-react.js +136 -223
  50. package/dist/tsembed.es.js +137 -224
  51. package/dist/tsembed.js +136 -223
  52. package/dist/visual-embed-sdk-react-full.d.ts +106 -204
  53. package/dist/visual-embed-sdk-react.d.ts +106 -204
  54. package/dist/visual-embed-sdk.d.ts +106 -202
  55. package/lib/src/embed/app.d.ts +56 -0
  56. package/lib/src/embed/app.d.ts.map +1 -1
  57. package/lib/src/embed/app.js +48 -9
  58. package/lib/src/embed/app.js.map +1 -1
  59. package/lib/src/embed/app.spec.js +322 -7
  60. package/lib/src/embed/app.spec.js.map +1 -1
  61. package/lib/src/embed/liveboard.d.ts +58 -1
  62. package/lib/src/embed/liveboard.d.ts.map +1 -1
  63. package/lib/src/embed/liveboard.js +60 -9
  64. package/lib/src/embed/liveboard.js.map +1 -1
  65. package/lib/src/embed/liveboard.spec.js +206 -0
  66. package/lib/src/embed/liveboard.spec.js.map +1 -1
  67. package/lib/src/embed/ts-embed.d.ts +7 -0
  68. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  69. package/lib/src/embed/ts-embed.js +61 -7
  70. package/lib/src/embed/ts-embed.js.map +1 -1
  71. package/lib/src/types.d.ts +37 -6
  72. package/lib/src/types.d.ts.map +1 -1
  73. package/lib/src/types.js +35 -4
  74. package/lib/src/types.js.map +1 -1
  75. package/lib/src/utils/processTrigger.js +2 -1
  76. package/lib/src/utils/processTrigger.js.map +1 -1
  77. package/lib/src/utils.d.ts +6 -0
  78. package/lib/src/utils.d.ts.map +1 -1
  79. package/lib/src/utils.js +21 -2
  80. package/lib/src/utils.js.map +1 -1
  81. package/lib/src/utils.spec.js +213 -2
  82. package/lib/src/utils.spec.js.map +1 -1
  83. package/lib/src/visual-embed-sdk.d.ts +106 -202
  84. package/package.json +1 -2
  85. package/src/embed/app.spec.ts +397 -8
  86. package/src/embed/app.ts +106 -12
  87. package/src/embed/liveboard.spec.ts +254 -1
  88. package/src/embed/liveboard.ts +109 -11
  89. package/src/embed/ts-embed.ts +84 -21
  90. package/src/types.ts +36 -5
  91. package/src/utils/processTrigger.ts +1 -1
  92. package/src/utils.spec.ts +250 -2
  93. package/src/utils.ts +28 -2
@@ -185,7 +185,10 @@ export class TsEmbed {
185
185
  */
186
186
  private fullscreenChangeHandler: (() => void) | null = null;
187
187
 
188
+ public id: string;
189
+
188
190
  constructor(domSelector: DOMSelector, viewConfig?: ViewConfig) {
191
+ this.id = Date.now().toString();
189
192
  this.el = getDOMNode(domSelector);
190
193
  this.eventHandlerMap = new Map();
191
194
  this.isError = false;
@@ -237,7 +240,7 @@ export class TsEmbed {
237
240
  * @param event The window message event
238
241
  */
239
242
  private getEventType(event: MessageEvent) {
240
- // eslint-disable-next-line no-underscore-dangle
243
+
241
244
  return event.data?.type || event.data?.__type;
242
245
  }
243
246
 
@@ -281,11 +284,11 @@ export class TsEmbed {
281
284
  */
282
285
  private isFullAppEmbedWithVisiblePrimaryNavbar(): boolean {
283
286
  const appViewConfig = this.viewConfig as any;
284
-
287
+
285
288
  // Check if this is a FullAppEmbed (AppEmbed)
286
289
  // showPrimaryNavbar defaults to true if not explicitly set to false
287
290
  return (
288
- appViewConfig.embedComponentType === 'AppEmbed'
291
+ appViewConfig.embedComponentType === 'AppEmbed'
289
292
  && appViewConfig.showPrimaryNavbar === true
290
293
  );
291
294
  }
@@ -310,6 +313,8 @@ export class TsEmbed {
310
313
 
311
314
  private subscribedListeners: Record<string, any> = {};
312
315
 
316
+ public isEmbedContainerLoaded = false;
317
+
313
318
  /**
314
319
  * Adds a global event listener to window for "message" events.
315
320
  * ThoughtSpot detects if a particular event is targeted to this
@@ -431,7 +436,8 @@ export class TsEmbed {
431
436
  private updateAuthToken = async (_: any, responder: any) => {
432
437
  const { authType } = this.embedConfig;
433
438
  let { autoLogin } = this.embedConfig;
434
- // Default autoLogin: true for cookieless if undefined/null, otherwise false
439
+ // Default autoLogin: true for cookieless if undefined/null, otherwise
440
+ // false
435
441
  autoLogin = autoLogin ?? (authType === AuthType.TrustedAuthTokenCookieless);
436
442
  if (autoLogin && authType === AuthType.TrustedAuthTokenCookieless) {
437
443
  try {
@@ -474,11 +480,65 @@ export class TsEmbed {
474
480
  notifyAuthFailure(AuthFailureType.IDLE_SESSION_TIMEOUT);
475
481
  };
476
482
 
483
+ private pendingEvents: Array<{ eventType: HostEvent, data: TriggerPayload<any, HostEvent>, onEventTriggered?: () => void }> = [];
484
+
485
+ protected getPreRenderObj<T extends TsEmbed>() {
486
+ const embedObj = (this.insertedDomEl as any)?.[this.embedNodeKey] as T;
487
+ if (embedObj === (this as any)) {
488
+ console.log('embedObj is same as this');
489
+ return null;
490
+ }
491
+ return (this.insertedDomEl as any)?.[this.embedNodeKey] as T;
492
+ }
493
+
494
+ private checkEmbedContainerLoaded() {
495
+ if (this.isEmbedContainerLoaded) return true;
496
+
497
+ const preRenderObj = this.getPreRenderObj<TsEmbed>();
498
+ if (preRenderObj && preRenderObj.isEmbedContainerLoaded) {
499
+ this.isEmbedContainerLoaded = true;
500
+ }
501
+
502
+ console.log('checkEmbedContainerLoaded', this.isEmbedContainerLoaded);
503
+
504
+ return this.isEmbedContainerLoaded;
505
+ }
506
+
507
+ private executePendingEvents() {
508
+ console.log('executePendingEvents', this.pendingEvents);
509
+ setTimeout(() => {
510
+ this.pendingEvents.forEach((event) => {
511
+ console.log('executing event', event.eventType, event.data);
512
+ this.trigger(event.eventType, event.data);
513
+ event.onEventTriggered?.();
514
+ });
515
+ this.pendingEvents = [];
516
+ }, 1000);
517
+ }
518
+ protected triggerAfterLoad(eventType: HostEvent, data: TriggerPayload<any, HostEvent>, onEventTriggered?: () => void) {
519
+ if (this.checkEmbedContainerLoaded()) {
520
+ console.log('triggerAfterLoad', eventType, data);
521
+ this.trigger(eventType, data);
522
+ onEventTriggered?.();
523
+ } else {
524
+ console.log('pushing to pendingEvents', eventType, data, this.getPreRenderObj());
525
+ this.pendingEvents.push({ eventType, data, onEventTriggered });
526
+ console.log('pendingEvents', this.pendingEvents);
527
+ }
528
+ }
529
+
477
530
  /**
478
531
  * Register APP_INIT event and sendback init payload
479
532
  */
480
533
  private registerAppInit = () => {
481
534
  this.on(EmbedEvent.APP_INIT, this.appInitCb, { start: false }, true);
535
+ this.on(EmbedEvent.AuthInit, () => {
536
+ console.log('AuthInit', this.getPreRenderObj());
537
+ this.isEmbedContainerLoaded = true;
538
+ console.log('isEmbedContainerLoaded', this.isEmbedContainerLoaded);
539
+ console.log('executePendingEvents', this.pendingEvents);
540
+ this.executePendingEvents();
541
+ }, { start: false }, true);
482
542
  this.on(EmbedEvent.AuthExpire, this.updateAuthToken, { start: false }, true);
483
543
  this.on(EmbedEvent.IdleSessionTimeout, this.idleSessionTimeout, { start: false }, true);
484
544
  };
@@ -798,8 +858,9 @@ export class TsEmbed {
798
858
  }
799
859
  });
800
860
  }
801
-
802
- // Setup fullscreen change handler after iframe is loaded and ready
861
+
862
+ // Setup fullscreen change handler after iframe is
863
+ // loaded and ready
803
864
  this.setupFullscreenChangeHandler();
804
865
  });
805
866
  this.iFrame.addEventListener('error', () => {
@@ -926,7 +987,7 @@ export class TsEmbed {
926
987
  const div = document.createElement('div');
927
988
  div.innerHTML = child;
928
989
  div.id = TS_EMBED_ID;
929
- // eslint-disable-next-line no-param-reassign
990
+
930
991
  child = div;
931
992
  }
932
993
  if (this.el.nextElementSibling?.id === TS_EMBED_ID) {
@@ -1070,11 +1131,11 @@ export class TsEmbed {
1070
1131
  if (this.isRendered) {
1071
1132
  logger.warn('Please register event handlers before calling render');
1072
1133
  }
1073
-
1134
+
1074
1135
  const callbacks = this.eventHandlerMap.get(messageType) || [];
1075
1136
  callbacks.push({ options, callback });
1076
1137
  this.eventHandlerMap.set(messageType, callbacks);
1077
-
1138
+
1078
1139
  return this;
1079
1140
  }
1080
1141
 
@@ -1174,7 +1235,7 @@ export class TsEmbed {
1174
1235
  }
1175
1236
  await this.isReadyForRenderPromise;
1176
1237
  this.isRendered = true;
1177
-
1238
+
1178
1239
  return this;
1179
1240
  }
1180
1241
 
@@ -1288,11 +1349,11 @@ export class TsEmbed {
1288
1349
  ) {
1289
1350
  logger.warn(
1290
1351
  `${viewConfig.embedComponentType || 'Component'} was pre-rendered with `
1291
- + `"${key}" as "${JSON.stringify(preRenderedObject.viewConfig[key])}" `
1292
- + `but a different value "${JSON.stringify(viewConfig[key])}" `
1293
- + 'was passed to the Embed component. '
1294
- + 'The new value provided is ignored, the value provided during '
1295
- + 'preRender is used.',
1352
+ + `"${key}" as "${JSON.stringify(preRenderedObject.viewConfig[key])}" `
1353
+ + `but a different value "${JSON.stringify(viewConfig[key])}" `
1354
+ + 'was passed to the Embed component. '
1355
+ + 'The new value provided is ignored, the value provided during '
1356
+ + 'preRender is used.',
1296
1357
  );
1297
1358
  }
1298
1359
  });
@@ -1318,8 +1379,11 @@ export class TsEmbed {
1318
1379
  return this.preRender(true);
1319
1380
  }
1320
1381
  this.validatePreRenderViewConfig(this.viewConfig);
1382
+ this.trigger(HostEvent.UpdateEmbedParams, this.viewConfig);
1321
1383
  }
1322
1384
 
1385
+ this.beforePrerenderVisible();
1386
+
1323
1387
  if (this.el) {
1324
1388
  this.syncPreRenderStyle();
1325
1389
  if (!this.viewConfig.doNotTrackPreRenderSize) {
@@ -1337,12 +1401,10 @@ export class TsEmbed {
1337
1401
  }
1338
1402
  }
1339
1403
 
1340
- this.beforePrerenderVisible();
1341
-
1342
1404
  removeStyleProperties(this.preRenderWrapper, ['z-index', 'opacity', 'pointer-events']);
1343
1405
 
1344
1406
  this.subscribeToEvents();
1345
-
1407
+
1346
1408
  // Setup fullscreen change handler for prerendered components
1347
1409
  if (this.iFrame) {
1348
1410
  this.setupFullscreenChangeHandler();
@@ -1431,7 +1493,7 @@ export class TsEmbed {
1431
1493
  private setupFullscreenChangeHandler() {
1432
1494
  const embedConfig = getEmbedConfig();
1433
1495
  const disableFullscreenPresentation = embedConfig?.disableFullscreenPresentation ?? true;
1434
-
1496
+
1435
1497
  if (disableFullscreenPresentation) {
1436
1498
  return;
1437
1499
  }
@@ -1444,7 +1506,8 @@ export class TsEmbed {
1444
1506
  const isFullscreen = !!document.fullscreenElement;
1445
1507
  if (!isFullscreen) {
1446
1508
  logger.info('Exited fullscreen mode - triggering ExitPresentMode');
1447
- // Only trigger if iframe is available and contentWindow is accessible
1509
+ // Only trigger if iframe is available and contentWindow is
1510
+ // accessible
1448
1511
  if (this.iFrame && this.iFrame.contentWindow) {
1449
1512
  this.trigger(HostEvent.ExitPresentMode);
1450
1513
  } else {
@@ -1540,6 +1603,6 @@ export class V1Embed extends TsEmbed {
1540
1603
  * Only for testing purposes.
1541
1604
  * @hidden
1542
1605
  */
1543
- // eslint-disable-next-line camelcase
1606
+
1544
1607
  public test__executeCallbacks = this.executeCallbacks;
1545
1608
  }
package/src/types.ts CHANGED
@@ -2707,6 +2707,12 @@ export enum EmbedEvent {
2707
2707
  * @version SDK : 1.40.0 | ThoughtSpot: 10.11.0.cl
2708
2708
  */
2709
2709
  ExitPresentMode = 'exitPresentMode',
2710
+ /**
2711
+ * Emitted when a user requests the full height lazy load data.
2712
+ * @version SDK : 1.39.0 | ThoughtSpot : 10.10.0.cl
2713
+ * @hidden
2714
+ */
2715
+ RequestVisibleEmbedCoordinates = 'requestVisibleEmbedCoordinates',
2710
2716
  /**
2711
2717
  * Emitted when spotter response is text data
2712
2718
  * @example
@@ -2802,7 +2808,7 @@ export enum EmbedEvent {
2802
2808
  * // create the liveboard embed.
2803
2809
  *
2804
2810
  * liveboardEmbed.trigger(HostEvent.UpdateRuntimeFilters, [
2805
- * { columnName: 'state, operator: RuntimeFilterOp.EQ, values: ['california']}
2811
+ * { columnName: 'state', operator: RuntimeFilterOp.EQ, values: ["california"]}
2806
2812
  * ]);
2807
2813
  * ```
2808
2814
  * @example
@@ -3011,12 +3017,12 @@ export enum HostEvent {
3011
3017
  * Works with Search and Liveboard embed.
3012
3018
  * @param - { columnId: string,
3013
3019
  * name: string,
3014
- * type: INT64/CHAR/DATE,
3015
- * dataType: ATTRIBUTE/MEASURE }
3020
+ * type: ATTRIBUTE/MEASURE,
3021
+ * dataType: INT64/CHAR/DATE }
3016
3022
  * @example
3017
3023
  * ```js
3018
3024
  * searchEmbed.trigger(HostEvent.OpenFilter,
3019
- * {column: { columnId: '<column-GUID>', name: 'column name', type: 'INT64', dataType: 'ATTRIBUTE'}})
3025
+ * {column: { columnId: '<column-GUID>', name: 'column name', type: 'ATTRIBUTE', dataType: 'INT64'}})
3020
3026
  * ```
3021
3027
  * @example
3022
3028
  * ```js
@@ -3984,7 +3990,19 @@ export enum HostEvent {
3984
3990
  * @version SDK: 1.40.0 | ThoughtSpot: 10.11.0.cl
3985
3991
  */
3986
3992
  ExitPresentMode = 'exitPresentMode',
3987
-
3993
+ /**
3994
+ * Triggers the full height lazy load data.
3995
+ * @example
3996
+ * ```js
3997
+ * liveboardEmbed.on(EmbedEvent.RequestVisibleEmbedCoordinates, (payload) => {
3998
+ * console.log(payload);
3999
+ * });
4000
+ * ```
4001
+ * @version SDK: 1.39.0 | ThoughtSpot: 10.10.0.cl
4002
+ *
4003
+ * @hidden
4004
+ */
4005
+ VisibleEmbedCoordinates = 'visibleEmbedCoordinates',
3988
4006
  /**
3989
4007
  * Trigger the *Ask Sage* action for visualizations
3990
4008
  * @example
@@ -3995,6 +4013,17 @@ export enum HostEvent {
3995
4013
  * @version SDK: 1.41.0 | ThoughtSpot: 10.12.0.cl
3996
4014
  */
3997
4015
  AskSpotter = 'askSpotter',
4016
+
4017
+ /**
4018
+ * @hidden
4019
+ * Triggers the update of the embed params.
4020
+ *
4021
+ * @example
4022
+ * ```js
4023
+ * liveboardEmbed.trigger(HostEvent.UpdateEmbedParams, viewConfig);
4024
+ * ```
4025
+ */
4026
+ UpdateEmbedParams = 'updateEmbedParams',
3998
4027
  }
3999
4028
 
4000
4029
  /**
@@ -4139,6 +4168,8 @@ export enum Param {
4139
4168
  PrimaryAction = 'primaryAction',
4140
4169
  isSpotterAgentEmbed = 'isSpotterAgentEmbed',
4141
4170
  IsLiveboardStylingAndGroupingEnabled = 'isLiveboardStylingAndGroupingEnabled',
4171
+ IsLazyLoadingForEmbedEnabled = 'isLazyLoadingForEmbedEnabled',
4172
+ RootMarginForLazyLoad = 'rootMarginForLazyLoad'
4142
4173
  }
4143
4174
 
4144
4175
  /**
@@ -31,7 +31,7 @@ function postIframeMessage(
31
31
  thoughtSpotHost: string,
32
32
  channel?: MessageChannel,
33
33
  ) {
34
- return iFrame.contentWindow.postMessage(message, thoughtSpotHost, [channel?.port2]);
34
+ return iFrame.contentWindow?.postMessage(message, thoughtSpotHost, [channel?.port2]);
35
35
  }
36
36
 
37
37
  export const TRIGGER_TIMEOUT = 30000;
package/src/utils.spec.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  handlePresentEvent,
17
17
  handleExitPresentMode,
18
18
  getTypeFromValue,
19
+ calculateVisibleElementData,
19
20
  } from './utils';
20
21
  import { RuntimeFilterOp } from './types';
21
22
  import { logger } from './utils/logger';
@@ -315,7 +316,7 @@ describe('unit test for utils', () => {
315
316
  });
316
317
 
317
318
  test('Object should be set if not', () => {
318
- // eslint-disable-next-line no-underscore-dangle
319
+
319
320
  (window as any)._tsEmbedSDK = null;
320
321
 
321
322
  storeValueInWindow('test', 'testValue');
@@ -341,7 +342,7 @@ describe('Fullscreen Utility Functions', () => {
341
342
 
342
343
  beforeEach(() => {
343
344
  jest.clearAllMocks();
344
-
345
+
345
346
  // Store and mock exitFullscreen
346
347
  originalExitFullscreen = document.exitFullscreen;
347
348
  document.exitFullscreen = jest.fn();
@@ -436,3 +437,250 @@ describe('Fullscreen Utility Functions', () => {
436
437
  });
437
438
  });
438
439
  });
440
+
441
+ describe('calculateVisibleElementData', () => {
442
+ let mockElement: HTMLElement;
443
+ let originalInnerHeight: number;
444
+ let originalInnerWidth: number;
445
+
446
+ beforeEach(() => {
447
+ // Store original window dimensions
448
+ originalInnerHeight = window.innerHeight;
449
+ originalInnerWidth = window.innerWidth;
450
+
451
+ // Mock window dimensions
452
+ Object.defineProperty(window, 'innerHeight', {
453
+ writable: true,
454
+ configurable: true,
455
+ value: 800,
456
+ });
457
+ Object.defineProperty(window, 'innerWidth', {
458
+ writable: true,
459
+ configurable: true,
460
+ value: 1200,
461
+ });
462
+
463
+ // Create mock element
464
+ mockElement = document.createElement('div');
465
+ });
466
+
467
+ afterEach(() => {
468
+ // Restore original window dimensions
469
+ Object.defineProperty(window, 'innerHeight', {
470
+ value: originalInnerHeight,
471
+ });
472
+ Object.defineProperty(window, 'innerWidth', {
473
+ value: originalInnerWidth,
474
+ });
475
+ });
476
+
477
+ it('should calculate data for fully visible element', () => {
478
+ // Mock getBoundingClientRect for element fully within viewport
479
+ jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
480
+ top: 100,
481
+ left: 150,
482
+ bottom: 300,
483
+ right: 400,
484
+ width: 250,
485
+ height: 200,
486
+ } as DOMRect);
487
+
488
+ const result = calculateVisibleElementData(mockElement);
489
+
490
+ expect(result).toEqual({
491
+ top: 0, // Not clipped from top
492
+ height: 200, // Full height visible
493
+ left: 0, // Not clipped from left
494
+ width: 250, // Full width visible
495
+ });
496
+ });
497
+
498
+ it('should calculate data for element clipped from top', () => {
499
+ // Mock getBoundingClientRect for element partially above viewport
500
+ jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
501
+ top: -50,
502
+ left: 100,
503
+ bottom: 150,
504
+ right: 400,
505
+ width: 300,
506
+ height: 200,
507
+ } as DOMRect);
508
+
509
+ const result = calculateVisibleElementData(mockElement);
510
+
511
+ expect(result).toEqual({
512
+ top: 50, // Clipped 50px from top
513
+ height: 150, // 150px visible height (0 to 150)
514
+ left: 0, // Not clipped from left
515
+ width: 300, // Full width visible
516
+ });
517
+ });
518
+
519
+ it('should calculate data for element clipped from left', () => {
520
+ // Mock getBoundingClientRect for element partially left of viewport
521
+ jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
522
+ top: 100,
523
+ left: -80,
524
+ bottom: 300,
525
+ right: 200,
526
+ width: 280,
527
+ height: 200,
528
+ } as DOMRect);
529
+
530
+ const result = calculateVisibleElementData(mockElement);
531
+
532
+ expect(result).toEqual({
533
+ top: 0, // Not clipped from top
534
+ height: 200, // Full height visible
535
+ left: 80, // Clipped 80px from left
536
+ width: 200, // 200px visible width (0 to 200)
537
+ });
538
+ });
539
+
540
+ it('should calculate data for element clipped from bottom', () => {
541
+ // Mock getBoundingClientRect for element extending below viewport
542
+ jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
543
+ top: 600,
544
+ left: 100,
545
+ bottom: 950, // Extends beyond window height of 800
546
+ right: 400,
547
+ width: 300,
548
+ height: 350,
549
+ } as DOMRect);
550
+
551
+ const result = calculateVisibleElementData(mockElement);
552
+
553
+ expect(result).toEqual({
554
+ top: 0, // Not clipped from top
555
+ height: 200, // Only 200px visible (600 to 800)
556
+ left: 0, // Not clipped from left
557
+ width: 300, // Full width visible
558
+ });
559
+ });
560
+
561
+ it('should calculate data for element clipped from right', () => {
562
+ // Mock getBoundingClientRect for element extending beyond right edge
563
+ jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
564
+ top: 100,
565
+ left: 1000,
566
+ bottom: 300,
567
+ right: 1400, // Extends beyond window width of 1200
568
+ width: 400,
569
+ height: 200,
570
+ } as DOMRect);
571
+
572
+ const result = calculateVisibleElementData(mockElement);
573
+
574
+ expect(result).toEqual({
575
+ top: 0, // Not clipped from top
576
+ height: 200, // Full height visible
577
+ left: 0, // Not clipped from left
578
+ width: 200, // Only 200px visible width (1000 to 1200)
579
+ });
580
+ });
581
+
582
+ it('should calculate data for element clipped from multiple sides', () => {
583
+ // Mock getBoundingClientRect for element clipped from top and left
584
+ jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
585
+ top: -100,
586
+ left: -50,
587
+ bottom: 200,
588
+ right: 300,
589
+ width: 350,
590
+ height: 300,
591
+ } as DOMRect);
592
+
593
+ const result = calculateVisibleElementData(mockElement);
594
+
595
+ expect(result).toEqual({
596
+ top: 100, // Clipped 100px from top
597
+ height: 200, // 200px visible height (0 to 200)
598
+ left: 50, // Clipped 50px from left
599
+ width: 300, // 300px visible width (0 to 300)
600
+ });
601
+ });
602
+
603
+ it('should handle element completely outside viewport (above)', () => {
604
+ // Mock getBoundingClientRect for element completely above viewport
605
+ jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
606
+ top: -300,
607
+ left: 100,
608
+ bottom: -100,
609
+ right: 400,
610
+ width: 300,
611
+ height: 200,
612
+ } as DOMRect);
613
+
614
+ const result = calculateVisibleElementData(mockElement);
615
+
616
+ expect(result).toEqual({
617
+ top: 300, // Clipped 300px from top
618
+ height: 0, // No visible height (clamped from negative)
619
+ left: 0, // Not clipped from left
620
+ width: 300, // Full width would be visible if in viewport
621
+ });
622
+ });
623
+
624
+ it('should handle element completely outside viewport (left)', () => {
625
+ // Mock getBoundingClientRect for element completely left of viewport
626
+ jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
627
+ top: 100,
628
+ left: -400,
629
+ bottom: 300,
630
+ right: -100,
631
+ width: 300,
632
+ height: 200,
633
+ } as DOMRect);
634
+
635
+ const result = calculateVisibleElementData(mockElement);
636
+
637
+ expect(result).toEqual({
638
+ top: 0, // Not clipped from top
639
+ height: 200, // Full height would be visible if in viewport
640
+ left: 400, // Clipped 400px from left
641
+ width: 0, // No visible width (min(1200, -100) - max(-400, 0) = -100 - 0 = -100, but clamped)
642
+ });
643
+ });
644
+
645
+ it('should handle element larger than viewport', () => {
646
+ // Mock getBoundingClientRect for element larger than viewport
647
+ jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
648
+ top: -200,
649
+ left: -300,
650
+ bottom: 1000,
651
+ right: 1500,
652
+ width: 1800,
653
+ height: 1200,
654
+ } as DOMRect);
655
+
656
+ const result = calculateVisibleElementData(mockElement);
657
+
658
+ expect(result).toEqual({
659
+ top: 200, // Clipped 200px from top
660
+ height: 800, // Visible height equals window height
661
+ left: 300, // Clipped 300px from left
662
+ width: 1200, // Visible width equals window width
663
+ });
664
+ });
665
+
666
+ it('should handle element exactly at viewport boundaries', () => {
667
+ // Mock getBoundingClientRect for element at exact viewport boundaries
668
+ jest.spyOn(mockElement, 'getBoundingClientRect').mockReturnValue({
669
+ top: 0,
670
+ left: 0,
671
+ bottom: 800,
672
+ right: 1200,
673
+ width: 1200,
674
+ height: 800,
675
+ } as DOMRect);
676
+
677
+ const result = calculateVisibleElementData(mockElement);
678
+
679
+ expect(result).toEqual({
680
+ top: 0, // Not clipped from top
681
+ height: 800, // Full viewport height
682
+ left: 0, // Not clipped from left
683
+ width: 1200, // Full viewport width
684
+ });
685
+ });
686
+ });
package/src/utils.ts CHANGED
@@ -269,7 +269,7 @@ export const getOperationNameFromQuery = (query: string) => {
269
269
  export function removeTypename(obj: any) {
270
270
  if (!obj || typeof obj !== 'object') return obj;
271
271
 
272
- // eslint-disable-next-line no-restricted-syntax
272
+
273
273
  for (const key in obj) {
274
274
  if (key === '__typename') {
275
275
  delete obj[key];
@@ -300,7 +300,11 @@ export const setStyleProperties = (
300
300
  ): void => {
301
301
  if (!element?.style) return;
302
302
  Object.keys(styleProperties).forEach((styleProperty) => {
303
- element.style[styleProperty] = styleProperties[styleProperty].toString();
303
+ const styleKey = styleProperty as keyof CSSStyleDeclaration;
304
+ const value = styleProperties[styleKey];
305
+ if (value !== undefined) {
306
+ (element.style as any)[styleKey] = value.toString();
307
+ }
304
308
  });
305
309
  };
306
310
  /**
@@ -463,3 +467,25 @@ export const handleExitPresentMode = async (): Promise<void> => {
463
467
 
464
468
  logger.warn('Exit fullscreen API is not supported by this browser.');
465
469
  };
470
+
471
+ export const calculateVisibleElementData = (element: HTMLElement) => {
472
+ const rect = element.getBoundingClientRect();
473
+
474
+ const windowHeight = window.innerHeight;
475
+ const windowWidth = window.innerWidth;
476
+
477
+ const frameRelativeTop = Math.max(rect.top, 0);
478
+ const frameRelativeLeft = Math.max(rect.left, 0);
479
+
480
+ const frameRelativeBottom = Math.min(windowHeight, rect.bottom);
481
+ const frameRelativeRight = Math.min(windowWidth, rect.right);
482
+
483
+ const data = {
484
+ top: Math.max(0, rect.top * -1),
485
+ height: Math.max(0, frameRelativeBottom - frameRelativeTop),
486
+ left: Math.max(0, rect.left * -1),
487
+ width: Math.max(0, frameRelativeRight - frameRelativeLeft),
488
+ };
489
+
490
+ return data;
491
+ }