import { GridDefaults } from "constants/WidgetConstants"; import lottie from "lottie-web"; import confetti from "assets/lottie/binding.json"; import successAnimation from "assets/lottie/success-animation.json"; import { DATA_TREE_KEYWORDS, JAVASCRIPT_KEYWORDS, } from "constants/WidgetValidation"; import { GLOBAL_FUNCTIONS } from "./autocomplete/EntityDefinitions"; import { set } from "lodash"; import { Org } from "constants/orgConstants"; import { isPermitted, PERMISSION_TYPE, } from "pages/Applications/permissionHelpers"; import { User } from "constants/userConstants"; import { getAppsmithConfigs } from "configs"; const { intercomAppID } = getAppsmithConfigs(); export const snapToGrid = ( columnWidth: number, rowHeight: number, x: number, y: number, ) => { const snappedX = Math.round(x / columnWidth); const snappedY = Math.round(y / rowHeight); return [snappedX, snappedY]; }; export const formatBytes = (bytes: string | number) => { if (!bytes) return; const value = typeof bytes === "string" ? parseInt(bytes) : bytes; const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; if (value === 0) return "0 bytes"; const i = parseInt(String(Math.floor(Math.log(value) / Math.log(1024)))); if (i === 0) return bytes + " " + sizes[i]; return (value / Math.pow(1024, i)).toFixed(1) + " " + sizes[i]; }; export const getAbsolutePixels = (size?: string | null) => { if (!size) return 0; const _dex = size.indexOf("px"); if (_dex === -1) return 0; return parseInt(size.slice(0, _dex), 10); }; export const Directions: { [id: string]: string } = { UP: "up", DOWN: "down", LEFT: "left", RIGHT: "right", RIGHT_BOTTOM: "RIGHT_BOTTOM", }; export type Direction = typeof Directions[keyof typeof Directions]; const SCROLL_THRESHOLD = 20; export const getScrollByPixels = function( elem: { top: number; height: number; }, scrollParent: Element, child: Element, ): { scrollAmount: number; speed: number; } { const scrollParentBounds = scrollParent.getBoundingClientRect(); const scrollChildBounds = child.getBoundingClientRect(); const scrollAmount = 2 * GridDefaults.CANVAS_EXTENSION_OFFSET * GridDefaults.DEFAULT_GRID_ROW_HEIGHT; const topBuff = elem.top + scrollChildBounds.top > 0 ? elem.top + scrollChildBounds.top - SCROLL_THRESHOLD - scrollParentBounds.top : 0; const bottomBuff = scrollParentBounds.bottom - (elem.top + elem.height + scrollChildBounds.top + SCROLL_THRESHOLD); if (topBuff < SCROLL_THRESHOLD) { const speed = Math.max( (SCROLL_THRESHOLD - topBuff) / (2 * SCROLL_THRESHOLD), 0.1, ); return { scrollAmount: 0 - scrollAmount, speed, }; } if (bottomBuff < SCROLL_THRESHOLD) { const speed = Math.max( (SCROLL_THRESHOLD - bottomBuff) / (2 * SCROLL_THRESHOLD), 0.1, ); return { scrollAmount, speed, }; } return { scrollAmount: 0, speed: 0, }; }; export const scrollElementIntoParentCanvasView = ( el: { top: number; height: number; } | null, parent: Element | null, child: Element | null, ) => { if (el) { const scrollParent = parent; if (scrollParent && child) { const { scrollAmount: scrollBy } = getScrollByPixels( el, scrollParent, child, ); if (scrollBy < 0 && scrollParent.scrollTop > 0) { scrollParent.scrollBy({ top: scrollBy, behavior: "smooth" }); } if (scrollBy > 0) { scrollParent.scrollBy({ top: scrollBy, behavior: "smooth" }); } } } }; export const removeSpecialChars = (value: string, limit?: number) => { const separatorRegex = /\W+/; return value .split(separatorRegex) .join("_") .slice(0, limit || 30); }; export const flashElement = (el: HTMLElement) => { el.style.backgroundColor = "#FFCB33"; setTimeout(() => { el.style.backgroundColor = "transparent"; }, 1000); }; /** * flash elements with a background color * * @param id */ export const flashElementsById = (id: string | string[], timeout = 0) => { let ids: string[] = []; if (Array.isArray(id)) { ids = ids.concat(id); } else { ids = ids.concat([id]); } ids.forEach((id) => { setTimeout(() => { const el = document.getElementById(id); el?.scrollIntoView({ behavior: "smooth", block: "center", inline: "center", }); if (el) flashElement(el); }, timeout); }); }; export const resolveAsSpaceChar = (value: string, limit?: number) => { // ensures that all special characters are disallowed // while allowing all utf-8 characters const removeSpecialCharsRegex = /`|\~|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|\+|\=|\[|\{|\]|\}|\||\\|\'|\<|\,|\.|\>|\?|\/|\""|\;|\:|\s/; const duplicateSpaceRegex = /\s+/; return value .split(removeSpecialCharsRegex) .join(" ") .split(duplicateSpaceRegex) .join(" ") .slice(0, limit || 30); }; export const isMac = () => { const platform = typeof navigator !== "undefined" ? navigator.platform : undefined; return !platform ? false : /Mac|iPod|iPhone|iPad/.test(platform); }; /** * Removes the trailing slashes from the path * @param path * @example * ```js * let trimmedUrl = trimTrailingSlash('/url/') * console.log(trimmedUrl) //will output /url * ``` * @example * ```js * let trimmedUrl = trimTrailingSlash('/yet-another-url//') * console.log(trimmedUrl) // will output /yet-another-url * ``` */ export const trimTrailingSlash = (path: string) => { const trailingUrlRegex = /\/+$/; return path.replace(trailingUrlRegex, ""); }; /** * checks if ellipsis is active * this function is meant for checking the existence of ellipsis by CSS. * Since ellipsis by CSS are not part of DOM, we are checking with scroll width\height and offsetidth\height. * ScrollWidth\ScrollHeight is always greater than the offsetWidth\OffsetHeight when ellipsis made by CSS is active. * * @param element */ export const isEllipsisActive = (element: HTMLElement | null) => { return ( element && (element.offsetWidth < element.scrollWidth || element.offsetHeight < element.scrollHeight) ); }; /** * converts array to sentences * for e.g - ['Pawan', 'Abhinav', 'Hetu'] --> 'Pawan, Abhinav and Hetu' * * @param arr string[] */ export const convertArrayToSentence = (arr: string[]) => { return arr.join(", ").replace(/,\s([^,]+)$/, " and $1"); }; /** * checks if the name is conflicting with * 1. API names, * 2. Queries name * 3. Javascript reserved names * 4. Few internal function names that are in the evaluation tree * * return if false name conflicts with anything from the above list * * @param name * @param invalidNames */ export const isNameValid = ( name: string, invalidNames: Record, ) => { return !( name in JAVASCRIPT_KEYWORDS || name in DATA_TREE_KEYWORDS || name in GLOBAL_FUNCTIONS || name in invalidNames ); }; /* * Filter out empty items from an array * for e.g - ['Pawan', undefined, 'Hetu'] --> ['Pawan', 'Hetu'] * * @param array any[] */ export const removeFalsyEntries = (arr: any[]): any[] => { return arr.filter(Boolean); }; /** * checks if variable passed is of type string or not * * for e.g -> 'Pawan' -> true * ['Pawan', 'Goku'] -> false * { name: "Pawan"} -> false */ export const isString = (str: any) => { return typeof str === "string" || str instanceof String; }; export const playOnboardingAnimation = () => { playLottieAnimation("#root", confetti); }; export const playOnboardingStepCompletionAnimation = () => { playLottieAnimation(".onboarding-step-indicator", successAnimation, { "background-color": "white", padding: "60px", }); }; const playLottieAnimation = ( selector: string, animation: any, styles?: any, ) => { const container: Element = document.querySelector(selector) as Element; if (!container) return; const el = document.createElement("div"); Object.assign(el.style, { position: "absolute", left: 0, right: 0, top: 0, bottom: 0, "z-index": 99, width: "100%", height: "100%", ...styles, }); container.appendChild(el); const animObj = lottie.loadAnimation({ container: el, animationData: animation, loop: false, }); const duration = (animObj.totalFrames / animObj.frameRate) * 1000; animObj.play(); setTimeout(() => { container.removeChild(el); }, duration); }; export const getSelectedText = () => { if (typeof window.getSelection === "function") { const selectionObj = window.getSelection(); return selectionObj && selectionObj.toString(); } }; /** * calculates and returns the scrollwidth * * @returns */ export const scrollbarWidth = () => { const scrollDiv = document.createElement("div"); scrollDiv.setAttribute( "style", "width: 100px; height: 100px; overflow: scroll; position:absolute; top:-9999px;", ); document.body.appendChild(scrollDiv); const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; document.body.removeChild(scrollDiv); return scrollbarWidth; }; // Flatten object // From { isValid: false, settings: { color: false}} // To { isValid: false, settings.color: false} export const flattenObject = (data: Record) => { const result: Record = {}; function recurse(cur: any, prop: any) { if (Object(cur) !== cur) { result[prop] = cur; } else if (Array.isArray(cur)) { for (let i = 0, l = cur.length; i < l; i++) recurse(cur[i], prop + "[" + i + "]"); if (cur.length == 0) result[prop] = []; } else { let isEmpty = true; for (const p in cur) { isEmpty = false; recurse(cur[p], prop ? prop + "." + p : p); } if (isEmpty && prop) result[prop] = {}; } } recurse(data, ""); return result; }; /** * renames key in object * * @param object * @param key * @param newKey * @returns */ export const renameKeyInObject = (object: any, key: string, newKey: string) => { if (object[key]) { set(object, newKey, object[key]); } return object; }; // Can be used to check if the user has developer role access to org export const getCanCreateApplications = (currentOrg: Org) => { const userOrgPermissions = currentOrg.userPermissions || []; const canManage = isPermitted( userOrgPermissions, PERMISSION_TYPE.CREATE_APPLICATION, ); return canManage; }; export const getIsSafeRedirectURL = (redirectURL: string) => { try { return new URL(redirectURL).origin === window.location.origin; } catch (e) { return false; } }; export function bootIntercom(user?: User) { if (intercomAppID && window.Intercom) { window.Intercom("boot", { app_id: intercomAppID, user_id: user?.username, name: user?.name, email: user?.email, }); } }