UNPKG

@applicaster/zapp-react-native-ui-components

Version:

Applicaster Zapp React Native ui components for the Quick Brick App

244 lines (194 loc) • 6.37 kB
import React, { useMemo } from "react"; import * as R from "ramda"; import validateColor from "validate-color"; import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils"; import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions"; import { masterCellLogger } from "../logger"; import { getCellState } from "../../Cell/utils"; import { getColorFromData } from "@applicaster/zapp-react-native-utils/cellUtils"; import { isCellSelected, useBehaviorUpdate } from "./behaviorProvider"; const hasElementSpecificViewType = (viewType) => (element) => { if (R.isNil(element)) { return false; } if (element.type === viewType) { return true; } // eslint-disable-next-line @typescript-eslint/no-use-before-define return hasElementsSpecificViewType(viewType)(element.elements); }; const logWarning = R.curry( (colorValueFromCellStyle, style, entry, colorValueFromEntry) => { if (R.isNil(colorValueFromEntry)) { masterCellLogger.warn({ message: `Cannot resolve property ${colorValueFromCellStyle} from the entry.`, data: { configurationValue: colorValueFromCellStyle, entry, }, }); } return style; } ); export const hasElementsSpecificViewType = (viewType) => (elements) => { if (R.isNil(elements) || R.isEmpty(elements)) { return false; } return R.any(hasElementSpecificViewType(viewType))(elements); }; function resolveColorForProp(entry, style, colorProp) { const colorFromProp = style[colorProp]; const nestedEntryValue = R.path(colorFromProp.split("."), entry); const color = colorFromProp.replace(".00", "").replace(".0", ""); // https://github.com/dreamyguy/validate-color/issues/44 if (nestedEntryValue === undefined && !validateColor(color)) { logWarning(colorFromProp, style, entry, nestedEntryValue); } const colorValue = getColorFromData({ data: entry, valueFromLayout: colorFromProp, }); if (!colorValue) { logWarning(colorProp, style, entry, colorValue); return style; } return { ...style, [colorProp]: colorValue }; } export function resolveColor(entry, style) { if (style === null || style === undefined) { return style; } // TODO can be optimized to remove 3 O(n) loops const styleKeys = Object.keys(style); const colorKeys = styleKeys.filter((key) => /color/i.test(key)); return colorKeys.reduce((acc, value) => { return { ...style, ...resolveColorForProp(entry, acc, value) }; }, style); } export function isVideoPreviewEnabled({ enable_video_preview = false, player_screen_id = null, }: { enable_video_preview: boolean; player_screen_id: string | null; }) { return enable_video_preview && !R.isEmpty(player_screen_id); } export const useFilterChildren = < T extends { props: { item: any; pluginIdentifier: string; }; }, >( children: T[] ): T[] => { const actions = useActions(""); const filteredChildren = children.filter((child) => { const item = child.props.item; const actionIdentifier = child.props.pluginIdentifier; const action = actions.actions[actionIdentifier]; // context value of specific plugin if (action?.module && action.module.context) { const currentValue = action.module.context._currentValue; return currentValue?.isActionAvailable ? currentValue.isActionAvailable(item) : true; } masterCellLogger.error({ message: `Action plugin for ${actionIdentifier} could not be found, check the configuration of your action button`, data: { item, action: child.props.pluginIdentifier }, }); return false; }); return filteredChildren; }; export const insertBetween = (separator, items) => { return items.reduce((acc, item, index) => { if (index + 1 >= items.length) { // last element acc.push(item); return acc; } acc.push(item); acc.push(separator(index)); return acc; }, []); }; const recursiveCloneElement = (focused: boolean) => (element) => { const { children, ...otherProps } = element.props; const state = focused ? "focused" : "default"; return React.cloneElement(element, { ...otherProps, state, // eslint-disable-next-line @typescript-eslint/no-use-before-define children: recursiveCloneElementsWithState(focused, children), }); }; export const recursiveCloneElementsWithState = (focused: boolean, children) => { if (R.isNil(children) || R.isEmpty(children)) { return undefined; } return React.Children.map(children, recursiveCloneElement(focused)); }; const next = (currentIndex, items) => items[currentIndex + 1]; const previous = (currentIndex, items) => items[currentIndex - 1]; export const cloneElementsWithIds = (ids, children) => { if (R.isNil(children) || R.isEmpty(children)) { return undefined; } return React.Children.map(children, (element, index) => React.cloneElement(element, { nextFocusLeft: previous(index, ids), nextFocusRight: next(index, ids), }) ); }; export const getFocusedButtonId = (focusable) => { return platformSelect({ tvos: R.path(["itemID"], focusable), default: R.path(["props", "id"], focusable), }); }; export const isSelected = (item: ZappEntry, behavior?: Behavior) => { return isCellSelected(item, behavior); }; export const useCellState = ({ item, behavior, focused, }: { item: ZappEntry; behavior: Behavior; focused: boolean; }): CellState => { const lastUpdate = useBehaviorUpdate(behavior); const _isSelected = useMemo( () => isSelected(item, behavior), [behavior, item, lastUpdate] ); return getCellState({ focused, selected: _isSelected }); }; export const hasFocusableInsideBuilder = (elementsBuilder) => (item) => { const elements = elementsBuilder({ entry: item }); return R.anyPass([ hasElementsSpecificViewType("ButtonContainerView"), hasElementsSpecificViewType("FocusableView"), ])(elements); }; export function getEntryState(state, selected) { if (state === "focused_selected") { return state; } if (state === "focused" && selected) { return "focused_selected"; } if (state === "focused" && !selected) { return "focused"; } if (state === "selected") { return "selected"; } return selected ? "selected" : "default"; }