@thoughtspot/visual-embed-sdk
Version:
ThoughtSpot Embed SDK
465 lines • 18.7 kB
JavaScript
/**
* Copyright (c) 2023
*
* Common utility functions for ThoughtSpot Visual Embed SDK
* @summary Utils
* @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatTemplate = exports.calculateVisibleElementData = exports.handleExitPresentMode = exports.handlePresentEvent = exports.resetValueFromWindow = exports.arrayIncludesString = exports.getValueFromWindow = exports.storeValueInWindow = exports.getTypeFromValue = exports.isUndefined = exports.removeStyleProperties = exports.setStyleProperties = exports.removeTypename = exports.getOperationNameFromQuery = exports.deepMerge = exports.getDOMNode = exports.getRuntimeFilters = exports.getCustomisations = exports.checkReleaseVersionInBeta = exports.setAttributes = exports.embedEventStatus = exports.getOffsetTop = exports.getEncodedQueryParamsString = exports.getRedirectUrl = exports.appendToUrlHash = exports.getSSOMarker = exports.getCssDimension = exports.getQueryParamString = exports.getRuntimeParameters = exports.getFilterQuery = void 0;
const tslib_1 = require("tslib");
const ts_deepmerge_1 = tslib_1.__importDefault(require("ts-deepmerge"));
const logger_1 = require("./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
*/
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;
};
exports.getFilterQuery = getFilterQuery;
/**
* Construct a runtime parameter override query string from the given option.
* @param runtimeParameters
*/
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;
};
exports.getRuntimeParameters = getRuntimeParameters;
/**
* 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
*/
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;
};
exports.getQueryParamString = getQueryParamString;
/**
* Get a string representation of a dimension value in CSS
* If numeric, it is considered in pixels.
* @param value
*/
const getCssDimension = (value) => {
if (typeof value === 'number') {
return `${value}px`;
}
return value;
};
exports.getCssDimension = getCssDimension;
const getSSOMarker = (markerId) => {
const encStringToAppend = encodeURIComponent(markerId);
return `tsSSOMarker=${encStringToAppend}`;
};
exports.getSSOMarker = getSSOMarker;
/**
* Append a string to a URL's hash fragment
* @param url A URL
* @param stringToAppend The string to append to the URL hash
*/
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;
};
exports.appendToUrlHash = appendToUrlHash;
/**
*
* @param url
* @param stringToAppend
* @param path
*/
function getRedirectUrl(url, stringToAppend, path = '') {
const targetUrl = path ? new URL(path, window.location.origin).href : url;
return (0, exports.appendToUrlHash)(targetUrl, stringToAppend);
}
exports.getRedirectUrl = getRedirectUrl;
const getEncodedQueryParamsString = (queryString) => {
if (!queryString) {
return queryString;
}
return btoa(queryString).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
};
exports.getEncodedQueryParamsString = getEncodedQueryParamsString;
const getOffsetTop = (element) => {
const rect = element.getBoundingClientRect();
return rect.top + window.scrollY;
};
exports.getOffsetTop = getOffsetTop;
exports.embedEventStatus = {
START: 'start',
END: 'end',
};
const setAttributes = (element, attributes) => {
Object.keys(attributes).forEach((key) => {
element.setAttribute(key, attributes[key].toString());
});
};
exports.setAttributes = setAttributes;
const isCloudRelease = (version) => version.endsWith('.cl');
/* For Search Embed: ReleaseVersionInBeta */
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;
};
exports.checkReleaseVersionInBeta = checkReleaseVersionInBeta;
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;
};
exports.getCustomisations = getCustomisations;
const getRuntimeFilters = (runtimefilters) => (0, exports.getFilterQuery)(runtimefilters || []);
exports.getRuntimeFilters = getRuntimeFilters;
/**
* Gets a reference to the DOM node given
* a selector.
* @param domSelector
*/
function getDOMNode(domSelector) {
return typeof domSelector === 'string' ? document.querySelector(domSelector) : domSelector;
}
exports.getDOMNode = getDOMNode;
const deepMerge = (target, source) => (0, ts_deepmerge_1.default)(target, source);
exports.deepMerge = deepMerge;
const getOperationNameFromQuery = (query) => {
const regex = /(?:query|mutation)\s+(\w+)/;
const matches = query.match(regex);
return matches === null || matches === void 0 ? void 0 : matches[1];
};
exports.getOperationNameFromQuery = getOperationNameFromQuery;
/**
*
* @param obj
*/
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;
}
exports.removeTypename = removeTypename;
/**
* 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);
*/
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();
}
});
};
exports.setStyleProperties = setStyleProperties;
/**
* 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);
*/
const removeStyleProperties = (element, styleProperties) => {
if (!(element === null || element === void 0 ? void 0 : element.style))
return;
styleProperties.forEach((styleProperty) => {
element.style.removeProperty(styleProperty);
});
};
exports.removeStyleProperties = removeStyleProperties;
const isUndefined = (value) => value === undefined;
exports.isUndefined = isUndefined;
// Return if the value is a string, double or boolean.
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 ['', ''];
};
exports.getTypeFromValue = getTypeFromValue;
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: *
*/
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;
}
exports.storeValueInWindow = storeValueInWindow;
/**
* 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.
*/
const getValueFromWindow = (key) => { var _a; return (_a = window === null || window === void 0 ? void 0 : window[sdkWindowKey]) === null || _a === void 0 ? void 0 : _a[key]; };
exports.getValueFromWindow = getValueFromWindow;
/**
* Check if an array includes a string value
* @param arr - The array to check
* @param key - The string to search for
* @returns boolean indicating if the string is found in the array
*/
const arrayIncludesString = (arr, key) => {
return arr.some(item => typeof item === 'string' && item === key);
};
exports.arrayIncludesString = arrayIncludesString;
/**
* 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
*/
function resetValueFromWindow(key) {
if (key in window[sdkWindowKey]) {
delete window[sdkWindowKey][key];
return true;
}
return false;
}
exports.resetValueFromWindow = resetValueFromWindow;
/**
* 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
*/
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_1.logger.warn(`Failed to enter fullscreen using ${method}:`, error);
}
}
}
logger_1.logger.error('Fullscreen API is not supported by this browser.');
};
exports.handlePresentEvent = handlePresentEvent;
/**
* Handle ExitPresentMode EmbedEvent by exiting fullscreen mode
*/
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_1.logger.warn(`Failed to exit fullscreen using ${method}:`, error);
}
}
}
logger_1.logger.warn('Exit fullscreen API is not supported by this browser.');
};
exports.handleExitPresentMode = handleExitPresentMode;
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;
};
exports.calculateVisibleElementData = calculateVisibleElementData;
/**
* Replaces placeholders in a template string with provided values.
* Placeholders should be in the format {key}.
* @param template - The template string with placeholders
* @param values - An object containing key-value pairs to replace placeholders
* @returns The template string with placeholders replaced
* @example
* formatTemplate('Hello {name}, you are {age} years old', { name: 'John', age: 30 })
* // Returns: 'Hello John, you are 30 years old'
*
* formatTemplate('Expected {type}, but received {actual}', { type: 'string', actual: 'number' })
* // Returns: 'Expected string, but received number'
*/
const formatTemplate = (template, values) => {
// This regex /\{(\w+)\}/g finds all placeholders in the format {word}
// and captures the word inside the braces for replacement.
return template.replace(/\{(\w+)\}/g, (match, key) => {
return values[key] !== undefined ? String(values[key]) : match;
});
};
exports.formatTemplate = formatTemplate;
//# sourceMappingURL=utils.js.map
;