UNPKG

@thoughtspot/visual-embed-sdk

Version:
465 lines 18.7 kB
"use strict"; /** * 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