@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
504 lines (428 loc) • 14.7 kB
text/typescript
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"),
};
};