@thoughtspot/visual-embed-sdk
Version:
ThoughtSpot Embed SDK
403 lines • 15.1 kB
JavaScript
/**
* Copyright (c) 2023
*
* Common utility functions for ThoughtSpot Visual Embed SDK
* @summary Utils
* @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
*/
import merge from 'ts-deepmerge';
import { logger } from './utils/logger';
/**
* Construct a runtime filters query string from the given filters.
* Refer to the following docs for more details on runtime filter syntax:
* https://cloud-docs.thoughtspot.com/admin/ts-cloud/apply-runtime-filter.html
* https://cloud-docs.thoughtspot.com/admin/ts-cloud/runtime-filter-operators.html
* @param runtimeFilters
*/
export const getFilterQuery = (runtimeFilters) => {
if (runtimeFilters && runtimeFilters.length) {
const filters = runtimeFilters.map((filter, valueIndex) => {
const index = valueIndex + 1;
const filterExpr = [];
filterExpr.push(`col${index}=${encodeURIComponent(filter.columnName)}`);
filterExpr.push(`op${index}=${filter.operator}`);
filterExpr.push(filter.values.map((value) => {
const encodedValue = typeof value === 'bigint' ? value.toString() : value;
return `val${index}=${encodeURIComponent(String(encodedValue))}`;
}).join('&'));
return filterExpr.join('&');
});
return `${filters.join('&')}`;
}
return null;
};
/**
* Construct a runtime parameter override query string from the given option.
* @param runtimeParameters
*/
export const getRuntimeParameters = (runtimeParameters) => {
if (runtimeParameters && runtimeParameters.length) {
const params = runtimeParameters.map((param, valueIndex) => {
const index = valueIndex + 1;
const filterExpr = [];
filterExpr.push(`param${index}=${encodeURIComponent(param.name)}`);
filterExpr.push(`paramVal${index}=${encodeURIComponent(param.value)}`);
return filterExpr.join('&');
});
return `${params.join('&')}`;
}
return null;
};
/**
* Convert a value to a string representation to be sent as a query
* parameter to the ThoughtSpot app.
* @param value Any parameter value
*/
const serializeParam = (value) => {
// do not serialize primitive types
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
return JSON.stringify(value);
};
/**
* Convert a value to a string:
* in case of an array, we convert it to CSV.
* in case of any other type, we directly return the value.
* @param value
*/
const paramToString = (value) => (Array.isArray(value) ? value.join(',') : value);
/**
* Return a query param string composed from the given params object
* @param queryParams
* @param shouldSerializeParamValues
*/
export const getQueryParamString = (queryParams, shouldSerializeParamValues = false) => {
const qp = [];
const params = Object.keys(queryParams);
params.forEach((key) => {
const val = queryParams[key];
if (val !== undefined) {
const serializedValue = shouldSerializeParamValues
? serializeParam(val)
: paramToString(val);
qp.push(`${key}=${serializedValue}`);
}
});
if (qp.length) {
return qp.join('&');
}
return null;
};
/**
* Get a string representation of a dimension value in CSS
* If numeric, it is considered in pixels.
* @param value
*/
export const getCssDimension = (value) => {
if (typeof value === 'number') {
return `${value}px`;
}
return value;
};
export const getSSOMarker = (markerId) => {
const encStringToAppend = encodeURIComponent(markerId);
return `tsSSOMarker=${encStringToAppend}`;
};
/**
* Append a string to a URL's hash fragment
* @param url A URL
* @param stringToAppend The string to append to the URL hash
*/
export const appendToUrlHash = (url, stringToAppend) => {
let outputUrl = url;
const encStringToAppend = encodeURIComponent(stringToAppend);
const marker = `tsSSOMarker=${encStringToAppend}`;
let splitAdder = '';
if (url.indexOf('#') >= 0) {
// If second half of hash contains a '?' already add a '&' instead of
// '?' which appends to query params.
splitAdder = url.split('#')[1].indexOf('?') >= 0 ? '&' : '?';
}
else {
splitAdder = '#?';
}
outputUrl = `${outputUrl}${splitAdder}${marker}`;
return outputUrl;
};
/**
*
* @param url
* @param stringToAppend
* @param path
*/
export function getRedirectUrl(url, stringToAppend, path = '') {
const targetUrl = path ? new URL(path, window.location.origin).href : url;
return appendToUrlHash(targetUrl, stringToAppend);
}
export const getEncodedQueryParamsString = (queryString) => {
if (!queryString) {
return queryString;
}
return btoa(queryString).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
};
export const getOffsetTop = (element) => {
const rect = element.getBoundingClientRect();
return rect.top + window.scrollY;
};
export const embedEventStatus = {
START: 'start',
END: 'end',
};
export const setAttributes = (element, attributes) => {
Object.keys(attributes).forEach((key) => {
element.setAttribute(key, attributes[key].toString());
});
};
const isCloudRelease = (version) => version.endsWith('.cl');
/* For Search Embed: ReleaseVersionInBeta */
export const checkReleaseVersionInBeta = (releaseVersion, suppressBetaWarning) => {
if (releaseVersion !== '' && !isCloudRelease(releaseVersion)) {
const splittedReleaseVersion = releaseVersion.split('.');
const majorVersion = Number(splittedReleaseVersion[0]);
const isBetaVersion = majorVersion < 8;
return !suppressBetaWarning && isBetaVersion;
}
return false;
};
export const getCustomisations = (embedConfig, viewConfig) => {
var _a, _b, _c, _d;
const customizationsFromViewConfig = viewConfig.customizations;
const customizationsFromEmbedConfig = embedConfig.customizations
|| embedConfig.customisations;
const customizations = {
style: {
...customizationsFromEmbedConfig === null || customizationsFromEmbedConfig === void 0 ? void 0 : customizationsFromEmbedConfig.style,
...customizationsFromViewConfig === null || customizationsFromViewConfig === void 0 ? void 0 : customizationsFromViewConfig.style,
customCSS: {
...(_a = customizationsFromEmbedConfig === null || customizationsFromEmbedConfig === void 0 ? void 0 : customizationsFromEmbedConfig.style) === null || _a === void 0 ? void 0 : _a.customCSS,
...(_b = customizationsFromViewConfig === null || customizationsFromViewConfig === void 0 ? void 0 : customizationsFromViewConfig.style) === null || _b === void 0 ? void 0 : _b.customCSS,
},
customCSSUrl: ((_c = customizationsFromViewConfig === null || customizationsFromViewConfig === void 0 ? void 0 : customizationsFromViewConfig.style) === null || _c === void 0 ? void 0 : _c.customCSSUrl)
|| ((_d = customizationsFromEmbedConfig === null || customizationsFromEmbedConfig === void 0 ? void 0 : customizationsFromEmbedConfig.style) === null || _d === void 0 ? void 0 : _d.customCSSUrl),
},
content: {
...customizationsFromEmbedConfig === null || customizationsFromEmbedConfig === void 0 ? void 0 : customizationsFromEmbedConfig.content,
...customizationsFromViewConfig === null || customizationsFromViewConfig === void 0 ? void 0 : customizationsFromViewConfig.content,
},
};
return customizations;
};
export const getRuntimeFilters = (runtimefilters) => getFilterQuery(runtimefilters || []);
/**
* Gets a reference to the DOM node given
* a selector.
* @param domSelector
*/
export function getDOMNode(domSelector) {
return typeof domSelector === 'string' ? document.querySelector(domSelector) : domSelector;
}
export const deepMerge = (target, source) => merge(target, source);
export const getOperationNameFromQuery = (query) => {
const regex = /(?:query|mutation)\s+(\w+)/;
const matches = query.match(regex);
return matches === null || matches === void 0 ? void 0 : matches[1];
};
/**
*
* @param obj
*/
export function removeTypename(obj) {
if (!obj || typeof obj !== 'object')
return obj;
for (const key in obj) {
if (key === '__typename') {
delete obj[key];
}
else if (typeof obj[key] === 'object') {
removeTypename(obj[key]);
}
}
return obj;
}
/**
* Sets the specified style properties on an HTML element.
* @param {HTMLElement} element - The HTML element to which the styles should be applied.
* @param {Partial<CSSStyleDeclaration>} styleProperties - An object containing style
* property names and their values.
* @example
* // Apply styles to an element
* const element = document.getElementById('myElement');
* const styles = {
* backgroundColor: 'red',
* fontSize: '16px',
* };
* setStyleProperties(element, styles);
*/
export const setStyleProperties = (element, styleProperties) => {
if (!(element === null || element === void 0 ? void 0 : element.style))
return;
Object.keys(styleProperties).forEach((styleProperty) => {
const styleKey = styleProperty;
const value = styleProperties[styleKey];
if (value !== undefined) {
element.style[styleKey] = value.toString();
}
});
};
/**
* Removes specified style properties from an HTML element.
* @param {HTMLElement} element - The HTML element from which the styles should be removed.
* @param {string[]} styleProperties - An array of style property names to be removed.
* @example
* // Remove styles from an element
* const element = document.getElementById('myElement');
* element.style.backgroundColor = 'red';
* const propertiesToRemove = ['backgroundColor'];
* removeStyleProperties(element, propertiesToRemove);
*/
export const removeStyleProperties = (element, styleProperties) => {
if (!(element === null || element === void 0 ? void 0 : element.style))
return;
styleProperties.forEach((styleProperty) => {
element.style.removeProperty(styleProperty);
});
};
export const isUndefined = (value) => value === undefined;
// Return if the value is a string, double or boolean.
export const getTypeFromValue = (value) => {
if (typeof value === 'string') {
return ['char', 'string'];
}
if (typeof value === 'number') {
return ['double', 'double'];
}
if (typeof value === 'boolean') {
return ['boolean', 'boolean'];
}
return ['', ''];
};
const sdkWindowKey = '_tsEmbedSDK';
/**
* Stores a value in the global `window` object under the `_tsEmbedSDK` namespace.
* @param key - The key under which the value will be stored.
* @param value - The value to store.
* @param options - Additional options.
* @param options.ignoreIfAlreadyExists - Does not set if value for key is set.
*
* @returns The stored value.
*
* @version SDK: 1.36.2 | ThoughtSpot: *
*/
export function storeValueInWindow(key, value, options = {}) {
if (!window[sdkWindowKey]) {
window[sdkWindowKey] = {};
}
if (options.ignoreIfAlreadyExists && key in window[sdkWindowKey]) {
return window[sdkWindowKey][key];
}
window[sdkWindowKey][key] = value;
return value;
}
/**
* Retrieves a stored value from the global `window` object under the `_tsEmbedSDK` namespace.
* @param key - The key whose value needs to be retrieved.
* @returns The stored value or `undefined` if the key is not found.
*/
export const getValueFromWindow = (key) => { var _a; return (_a = window === null || window === void 0 ? void 0 : window[sdkWindowKey]) === null || _a === void 0 ? void 0 : _a[key]; };
/**
* Resets the key if it exists in the `window` object under the `_tsEmbedSDK` key.
* Returns true if the key was reset, false otherwise.
* @param key - Key to reset
* @returns - boolean indicating if the key was reset
*/
export function resetValueFromWindow(key) {
if (key in window[sdkWindowKey]) {
delete window[sdkWindowKey][key];
return true;
}
return false;
}
/**
* Check if the document is currently in fullscreen mode
*/
const isInFullscreen = () => {
return !!(document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement);
};
/**
* Handle Present HostEvent by entering fullscreen mode
* @param iframe The iframe element to make fullscreen
*/
export const handlePresentEvent = async (iframe) => {
if (isInFullscreen()) {
return; // Already in fullscreen
}
// Browser-specific methods to enter fullscreen mode
const fullscreenMethods = [
'requestFullscreen',
'webkitRequestFullscreen',
'mozRequestFullScreen',
'msRequestFullscreen' // IE/Edge
];
for (const method of fullscreenMethods) {
if (typeof iframe[method] === 'function') {
try {
const result = iframe[method]();
await Promise.resolve(result);
return;
}
catch (error) {
logger.warn(`Failed to enter fullscreen using ${method}:`, error);
}
}
}
logger.error('Fullscreen API is not supported by this browser.');
};
/**
* Handle ExitPresentMode EmbedEvent by exiting fullscreen mode
*/
export const handleExitPresentMode = async () => {
if (!isInFullscreen()) {
return; // Not in fullscreen
}
const exitFullscreenMethods = [
'exitFullscreen',
'webkitExitFullscreen',
'mozCancelFullScreen',
'msExitFullscreen' // IE/Edge
];
// Try each method until one works
for (const method of exitFullscreenMethods) {
if (typeof document[method] === 'function') {
try {
const result = document[method]();
await Promise.resolve(result);
return;
}
catch (error) {
logger.warn(`Failed to exit fullscreen using ${method}:`, error);
}
}
}
logger.warn('Exit fullscreen API is not supported by this browser.');
};
export const calculateVisibleElementData = (element) => {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight;
const windowWidth = window.innerWidth;
const frameRelativeTop = Math.max(rect.top, 0);
const frameRelativeLeft = Math.max(rect.left, 0);
const frameRelativeBottom = Math.min(windowHeight, rect.bottom);
const frameRelativeRight = Math.min(windowWidth, rect.right);
const data = {
top: Math.max(0, rect.top * -1),
height: Math.max(0, frameRelativeBottom - frameRelativeTop),
left: Math.max(0, rect.left * -1),
width: Math.max(0, frameRelativeRight - frameRelativeLeft),
};
return data;
};
//# sourceMappingURL=utils.js.map