UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

504 lines (428 loc) • 14.7 kB
import * as R from "ramda"; import dayjs from "dayjs"; import validateColor from "validate-color"; import { toNumberWithDefault, toNumberWithDefaultZero } from "../numberUtils"; import { transformColorCode as fixColorHexCode } from "../transform"; import { capitalize, isString } from "../stringUtils"; import { cellUtilsLogger } from "./logger"; import { isWeb } from "../reactUtils"; import { isNotNil } from "../reactUtils/helpers"; const CUSTOM_KEY = "other"; /** * Gets key for label if other return custom key, otherwise returns selected data key. * @param {String} dataKey selected data key for label. * @param {String} customKey custom data key for label. * @returns {String[]} keys splitted by '.' separator */ export const getKeyForLabel = (dataKey, customKey) => { const keyToUse = dataKey === CUSTOM_KEY ? customKey : dataKey; if (keyToUse === null || keyToUse === undefined) { return []; } return keyToUse.split("."); }; /** * This method will return true if the argument passed to it is either empty or nil * The method prevents zero from being evaluated as falsey in an || condition * i.e. - NaN, null, {}, "", [], should all return true * @returns {Boolean} returns true if empty or nil * @param value */ export const isEmptyOrNil = (value: any): boolean => Number.isNaN(value) || value == null || R.isEmpty(value); /** * This method is used to find the appropriate label for any given text field. * We don't know if users want to use properties from feed, or write the value * so we give them the option to do both. * - dataKeyValue is either a path to a string, or a string that will be used as a label. * - customDataKeyValue follows the same as above * @param {String} dataKeyValue - path (extensions.free) or string (More). * @param {String} customDataKeyValue - path (extensions.duration) or string (Watch Series). * @param {Object} entry - feed item or entry * @returns {String} label - a string retrieved either from path, or default to string. */ export const getLabel = (dataKeyValue, customDataKeyValue) => (entry) => { const label = getKeyForLabel(dataKeyValue, customDataKeyValue); const isPath = label && R.length(label) > 0; const fallback = isEmptyOrNil(label) ? "" : R.head(label); try { return isPath ? R.pathOr(fallback, label, entry) : fallback; } catch (error) { cellUtilsLogger.warning({ data: { error, dataKeyValue, customDataKeyValue }, message: error.message, }); return ""; } }; /** * Gets a string like "16x9" and returns a number value (float) for aspect ratio * @param {String} ratioString specified on zapp * @param {String} customRatio user defined aspect ratio string * @returns {Number} aspectRatio, floating point number extracted from string, or from cell default */ export const getAspectRatio = (ratioString, customRatio = "16x9") => { const defaultAspectRatio = "16x9"; const replaceColon = (ratio) => ratio && R.replace(":", "x", R.__)(ratio); const conversionFn = R.compose(R.apply(R.divide), R.split("x"), R.toLower); const aspectRatio = ratioString === "other" ? replaceColon(customRatio) : replaceColon(ratioString); const convertRatio = R.compose( R.when(R.equals(NaN), R.always(conversionFn(defaultAspectRatio))), Number, conversionFn ); return aspectRatio ? convertRatio(aspectRatio) : convertRatio(defaultAspectRatio); }; /** * This method builds upon isEmptyOrNil method and lets you check if the value is empty * If it is empty, return the fallback value provided * If not return the original value, 1st argument * @param {*} value - value will be evaluated, if it has a value it will be returned * @param {*} fallback - if value was empty or nil, return this fallback * @returns {*} returns value or fallback */ export const ifEmptyUseFallback = (value, fallback) => { if (isEmptyOrNil(value)) { return fallback; } else { return value; } }; /** * This method transforms text for platforms that do not support the react native prop * 'default' returns original string, otherwise it returns transformed string * @param {String} transformType - 'default', 'uppercase', 'lowercase', 'capitalize * @param {String} value - string to be transformed * @returns {String} returns original string or transformed string */ const capitalizeArray = R.compose(R.join(" "), R.map(capitalize)); export const textTransform = (transformType, value) => { const transformation = ifEmptyUseFallback(transformType, "default"); if (!isString(value) || transformation === "default") return value; if (transformation === "uppercase") { return value.toUpperCase(); } if (transformation === "lowercase") { return value.toLowerCase(); } if (transformation === "capitalize") { const valueArray = value.trim().split(" "); if (valueArray.length > 1) { return capitalizeArray(valueArray); } return capitalize(value); } }; /** * Get cell width specified at component level * Used for components that do not use cell scaling * @param {Object} componentStyles i.e. { horizontal_list_cell_width: 384 } * @returns {Number} i.e. 384 */ export const getCellWidth = (componentStyles) => { const DEFAULT_CELL_WIDTH = 1920; const cellWidth = R.compose( R.prop(R.__, componentStyles), R.find(R.includes("cell_width")), R.keys )(componentStyles); return Number(cellWidth) || DEFAULT_CELL_WIDTH; }; /** * Get cell height with given asepect ratio and width * @param {Number} width i.e. 1920 * @param {Number} aspectRatio i.e. 1.7777 * @returns {Number | String} i.e. 1080 */ export const getHeight = (width, aspectRatio) => { const DEFAULT_CELL_HEIGHT = 216; const height = R.divide(width, aspectRatio); return toNumberWithDefault(DEFAULT_CELL_HEIGHT, height); }; /** * * This function for provided number of seconds returns formatted duration time mm:ss * @param {number} seconds * @returns {string} duration */ export const getDurationInMinutes = (seconds?: number): string => { if (!seconds) return "00:00"; const minutes = Math.floor(Number(seconds) / 60); const secondsReminder = Math.floor(Number(seconds)) % 60; const pad = (num: number): string => `${num < 10 ? "0" : ""}${num}`; const hours = Math.floor(Number(minutes) / 60); const minutesReminder = Number(minutes) % 60; if (hours < 1) { return `${pad(minutes)}:${pad(secondsReminder)}`; } return `${pad(hours)}:${pad(minutesReminder)}:${pad(secondsReminder)}`; }; const isTruthy = R.either(R.equals(true), R.equals("true")); /** * This functionr returns the url of the lock badge depending on the * data in the cell. We look for the value of the lock_badge_data_key if it exists, * and return undefined if it doesn't. if the key doesn't exist, the badge isn't shown. * If the key exists, a truthy value will show the unlocked_badge, and a falsy value * will show the locked_badge. This is because the default key is called "free", and a true * value expects to show the unlocked badge, when a falsy value should show the lock. While * this makes sense for content locking, it could prove counter-intuitive when using this * mechanism to toggle other badges. * @param {Function} getValueFromCellConfig - function to get the value for a specific key * from the cell config * @param {Object} data - data used to populate the cell. always a ZappEntry * @param {string} lockBadgeDataKey - key pointing to the property in the entry used to decide * if the lock badge should be shown * @returns {string | null} */ export const getLockBadge = R.curryN( 2, (getValueFromCellConfig, data, lockBadgeDataKey) => { try { return R.compose( R.unless( R.isNil, R.ifElse( isTruthy, R.always(getValueFromCellConfig("unlocked_badge")), R.always(getValueFromCellConfig("locked_badge")) ) ), R.pathOr(null, R.split(".", lockBadgeDataKey)) )(data); } catch (e) { return null; } } ); /** * * This function format date according to provided * @param {string} format - format mask * @param {string} text - string to be formatted * @returns {string} formatted string */ export const dateFormat = (format, text) => { if (isEmptyOrNil(format)) { cellUtilsLogger.debug({ data: { format, text }, message: "Date format not provided", }); return text; } const dayjsObject = dayjs(text); if (dayjsObject.isValid()) { return dayjsObject.format(format); } else { cellUtilsLogger.debug({ data: { format, text }, message: "Text is not valid date", }); return text; } }; /** * * This function returns the intended custom badge for tv cells * @param {function} value - return the needed value of style from cell styles layout * @param {boolean} focused - whether the focused type should be returned */ export const customTypesMap = (value, focused) => { const suffix = focused ? "_focused" : ""; return { [value("content_badge_content_type_custom_badge_1")]: value( `content_badge_custom_badge_1${suffix}` ), [value("content_badge_content_type_custom_badge_2")]: value( `content_badge_custom_badge_2${suffix}` ), [value("content_badge_content_type_custom_badge_3")]: value( `content_badge_custom_badge_3${suffix}` ), [value("content_badge_content_type_custom_badge_4")]: value( `content_badge_custom_badge_4${suffix}` ), [value("content_badge_content_type_custom_badge_5")]: value( `content_badge_custom_badge_5${suffix}` ), }; }; export const ifElseFocusedSelected = ( state: string, focusedValue, selectedValue, focusedAndSelectedValue, defaultValue ) => { switch (state) { case "focused": return focusedValue; case "selected": return selectedValue; case "focused_selected": return focusedAndSelectedValue; default: return defaultValue; } }; /** * Gets a style key from the style object and returns a value that can be used in styles * @param {String} key i.e. button_icon_width * @param {Object} stylesObj styles object with keys from zapp { button_icon_width: "10" } * @returns {*} value, returns any type that can be used as styles */ export const getValue = (stylesObj) => (key) => { return R.propOr(null, key, stylesObj); }; export const CELL_CONFIG_ID_PATH = ["styles", "cell_plugin_configuration_id"]; export const getCellConfigPath = R.path(CELL_CONFIG_ID_PATH); export const getPropComponentType = R.prop("component_type"); /** * * Returns second argument if state equals focused * returns third otherwise * curried * @param {string} state * @param {*} focusedValue * @param {*} defaultValue */ export const ifElseFocused = R.curry( (state: string, focusedValue, defaultValue) => state === "focused" ? focusedValue : defaultValue ); export const ifElseSelected = R.curry( (state: string, selectedValue, defaultValue) => { switch (state) { case "selected": return selectedValue; case "focused_selected": return selectedValue; default: return defaultValue; } } ); /** * Map textAlign values to their corresponding alignSelf values * This is so that we could use the same field for both properties * @param {String} horizontalPosition i.e. bottom_text_label_3 * @returns {String} alignSelf property i.e. flex-end, flex-start, center */ export const mapSelfAlignment = (horizontalPosition) => { switch (horizontalPosition) { case "left": return "flex-start"; case "right": return "flex-end"; case "center": return "center"; case "middle": return "center"; default: return "flex-start"; } }; export const castAndFallbackNumber = toNumberWithDefaultZero; const DEFAULT_COLOR = "#FFFFFF"; export const castAndFallbackColor = R.ifElse( R.isNil, R.always(DEFAULT_COLOR), fixColorHexCode ); export const getTextTransform = (textTransform: string) => { if (R.isNil(textTransform) || R.isEmpty(textTransform)) { return "none"; } if (textTransform.toLowerCase() === "default") { return "none"; } return textTransform; }; export const withAntialising = () => { if (isWeb()) { return { WebkitFontSmoothing: "antialiased", MozOsxFontSmoothing: "grayscale", }; } return {}; }; export const compact = (xs) => xs.filter(isNotNil); export const mergeConfiguration = (configuration, config) => R.evolve({ configuration }, config); export const extractCellConfiguration = (manifestConfiguration) => (component, cellStyles) => { const cellConfigId = getCellConfigPath(component); const componentType = getPropComponentType(component); const cellStylesConfig = Object.assign(cellStyles[cellConfigId], { componentType, }); return mergeConfiguration(manifestConfiguration, cellStylesConfig); }; export const toResizeMode = ( imageSizing: string, isDisplayModeFixed: boolean ) => { if (isDisplayModeFixed) { const resizeMode = imageSizing === "fit" ? "contain" : "cover"; return { resizeMode, }; } return { resizeMode: "contain", }; }; type GetColorFromData = { data: Record<string, any>; valueFromLayout: string; }; export const getColorFromData = ({ data, valueFromLayout, }: GetColorFromData): string => { // Temporary hack to fix color validation when alpha is floating point number // https://github.com/dreamyguy/validate-color/issues/44 if (validateColor(valueFromLayout.replace(".00", ""))) { return valueFromLayout; } const pathValue = R.path(valueFromLayout.split("."), data); if (pathValue && validateColor(pathValue)) { return pathValue; } return valueFromLayout; }; export const getImageStyles = ({ image, value, }: { image: any; value: any; }) => { return { width: image.imageContentWidth, height: image.imageContentHeight, aspectRatio: image.aspectRatio, borderRadius: toNumberWithDefaultZero(value("image_corner_radius")), }; }; export const getImageContainerPaddingStyles = ({ value }: { value: any }) => { return { paddingTop: value("image_padding_top"), paddingBottom: value("image_padding_bottom"), paddingLeft: value("image_padding_left"), paddingRight: value("image_padding_right"), }; }; export const getImageContainerMarginStyles = ({ value }: { value: any }) => { return { marginTop: value("image_margin_top"), marginBottom: value("image_margin_bottom"), marginLeft: value("image_margin_left"), marginRight: value("image_margin_right"), }; };