@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
text/typescript
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";
}