@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
343 lines (282 loc) • 9.63 kB
JavaScript
import { allPass, pathOr, startsWith, isNil } from "ramda";
import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
import { isZero } from "@applicaster/zapp-react-native-utils/numberUtils";
import { QUICK_BRICK_NAVBAR } from "@applicaster/quick-brick-core/const";
import { rejectItemByPath } from "../../../../objectUtils";
import {
measureDistance,
measureEuclidianDistanceToCenter,
} from "../../../../rectUtils";
import { capitalize } from "../../../../stringUtils";
export const FocusDirection = {
LEFT: 1,
TOP: 2,
RIGHT: 4,
BOTTOM: 8,
};
/**
* Function search down the tree for priority item in given children array.
* Priorities are:
* Priority 1: Currently selected item - like a menu item
* Priority 2: Previously focused item in the parent group
* Priority 3: Preferrable items - items which have been marked as preferable
* Priority 4: First item on the list - if by any accident above priories are not satisfied
* @private
* @param {Array} children
* @returns focusable priority item.
*/
export function findPriorityItem(children) {
let preferedGroups = getPriorityItems(children);
if (preferedGroups.length > 0 && !preferedGroups[0].children) {
return preferedGroups;
}
while (
preferedGroups.length > 0 &&
preferedGroups[0].children &&
preferedGroups[0].children.length > 0
) {
preferedGroups = preferedGroups.filter(
(it) => it.children && it.children.length > 0
);
preferedGroups = getPriorityItems(preferedGroups[0].children);
}
return preferedGroups;
}
/**
* Function search for currently selected item - like a menu item.
* @private
* @param {Array} children
* @returns array of selected items
*/
export function getSelectedItems(children) {
if (children) {
const selectedItems = children.filter(
(obj) => obj && obj?.component?.props?.selected
);
return selectedItems;
}
return [];
}
/**
* Function search for previously focused item in the parent group.
* @private
* @param {Array} children
* @returns array of last focus items
*/
export function getLastFocusedItems(children) {
if (children) {
const lastFocusedItems = children.filter(
(obj) =>
obj &&
obj?.parent?.component?.isWithMemory?.() &&
obj.id === obj.parent.lastFocusedItem
);
return lastFocusedItems;
}
return [];
}
/**
* Function search for items which have been marked as preferable.
* @private
* @param {Array} children
* @returns array of prefereable focus items
*/
export function getPreferableItems(children) {
if (children) {
const preferredItems = children.filter(
(obj) => obj && obj.component?.props.preferredFocus
);
return preferredItems;
}
return [];
}
/**
* Function search for priority items in children array. Priorities are:
* Priority 1: Currently selected item - like a menu item
* Priority 2: Previously focused item in the parent group
* Priority 3: Preferrable items - items which have been marked as preferable
* Priority 4: First item on the list - if by any accident above priories are not satisfied
* @private
* @param {Array} children
* @returns array of pririty focus items
*/
export function getPriorityItems(children) {
if (!children) {
return [];
}
const selectedItems = getSelectedItems(children);
const lastFocusedItems = getLastFocusedItems(children);
const preferredItems = getPreferableItems(children);
if (selectedItems.length > 0) {
return selectedItems;
} else if (lastFocusedItems.length > 0) {
return lastFocusedItems;
} else if (preferredItems.length > 0) {
return preferredItems;
} else if (children.length > 0) {
return [children[0]];
}
return [];
}
const hasChildren = (target) =>
!(target?.component?.isGroup && isNilOrEmpty(target?.children));
const hasComponent = (target) => !!target?.component;
const hasComponentAndChildren = allPass([hasComponent, hasChildren]);
const positiveOrZero = (value) => value > 0 || isZero(value);
const excludeFromFocusSearching = pathOr(false, [
"component",
"props",
"excludeFromFocusSearching",
]);
const isNavBar = (target) => startsWith(QUICK_BRICK_NAVBAR, target?.id);
const ignoreMenuIfMultipleTargets = (nodes) => {
if (nodes.length > 1) {
return nodes.filter((n) => !isNavBar(n?.target));
}
return nodes;
};
const toDistance = ({ target, direction, comparisonRect }) => {
const componentRect = target.component?.getRect();
const orientationDistance = measureDistance(
direction,
comparisonRect,
componentRect
);
if (orientationDistance === -1) {
// ignores offscreen targets when moving up/down
return null;
}
const euclidDistance = measureEuclidianDistanceToCenter(
comparisonRect,
componentRect
);
return orientationDistance > 0 ? euclidDistance : orientationDistance;
};
/**
* Get target in shortest distance in specified direction
* @private
* @param {Object} direction of the navigation, which led to this focus change
* @param {Object} excludedNode node which should be excluded from target group
* @param {Array} targetNodes array of input target nodes
* @param {Object} comparisonRect rectangle to compare sides in direction.
* @returns {Object} target in direction.
*/
export function getTargetInDirection(
direction,
currentNode,
targetNodes,
comparisonRect
) {
const overridePropName = `nextFocus${capitalize(direction.value)}`;
const overrideTarget = currentNode?.component?.props[overridePropName];
if (overrideTarget) {
const target = targetNodes.find((n) => n?.id === overrideTarget);
if (target) return target;
}
const targets = rejectItemByPath(["id"], currentNode.id, targetNodes);
const distances = targets
.filter(hasComponentAndChildren)
.filter((target) => !excludeFromFocusSearching(target))
.map((target) => ({
distance: toDistance({ target, direction, comparisonRect }),
target,
}))
.filter(({ distance }) => positiveOrZero(distance));
const match = distances
.sort((a, b) => a.distance - b.distance) // sorts the distances
.filter(ignoreMenuIfMultipleTargets)[0]; // filters out multiple targets if necessary // gets the first element
return match?.target;
}
/**
* Find focusable node in direction from currentFocusedNode.
* If it does not find a node in current group it seach parent node
* incuding deep search inside simbilngs of parent. Search goes up the tree
* until find next focusable item or tree ends.
* @public
* @param {Object} direction of the navigation, which led to this focus change
* @param {Object} currentFocusNode currently focused node as start point of search
* @returns {Object} focusable node in direction.
*/
export function findFocusableNode(direction, currentFocusNode) {
const currentFocusable = currentFocusNode.component || null;
if (currentFocusable) {
let focusableNode = getTargetInDirection(
direction,
currentFocusNode,
currentFocusNode.parent.children,
currentFocusNode.component.getRect()
);
const parentHasPriority =
currentFocusNode.parent.component &&
hasPriorityInDirection(direction, currentFocusNode.parent.component);
const canFindParentSibling = findFocusableNode(
direction,
currentFocusNode.parent
);
if (parentHasPriority && canFindParentSibling) {
return canFindParentSibling;
} else if (focusableNode && focusableNode.component) {
// inside group
return focusableNode;
} else {
// outside group
let parentFocusableNode = currentFocusNode;
// going up the structure untill sibling node
while (
!focusableNode &&
parentFocusableNode &&
parentFocusableNode.parent &&
parentFocusableNode.parent.parent
) {
parentFocusableNode = parentFocusableNode.parent;
focusableNode = getTargetInDirection(
direction,
parentFocusableNode, // excluded sibling parent of current focuable
parentFocusableNode.parent.children, // siblings of parent
currentFocusNode.component.getRect()
);
}
if (focusableNode) {
const preferred = findPriorityItem(focusableNode.children);
return preferred[0];
}
}
}
}
/**
* Checks of component has priority in the direction. It is used to navigate
* outside of the group if group has priotity in specified direction.
* @public
* @param {Object} direction of the navigation, which led to this focus change
* @param {Object} component - object to check props.prioritiseFocusOn
* @returns {Boolean} TRUE if object has priority in direction.
* @returns {Boolean} FALSE if object has NO priority in direction.
*/
export function hasPriorityInDirection(direction, component) {
const { prioritiseFocusOn = 0 } = component.props;
const directionMapper = {
left: 1,
up: 2,
right: 4,
down: 8,
};
const mappedDirection = directionMapper[direction.value];
return (prioritiseFocusOn & mappedDirection) > 0;
}
export const getNodeBeforeRoot = (node) => {
if (isNil(node)) {
return undefined;
}
if (isNil(node.parent)) {
return undefined;
}
if (node.parent.id === "root") {
return node;
}
return getNodeBeforeRoot(node.parent);
};
export const haveSameParentBeforeRoot = (currentNode, nextNode) => {
const routeForCurrentNode = getNodeBeforeRoot(currentNode);
const routeForNextNode = getNodeBeforeRoot(nextNode);
return routeForCurrentNode?.id === routeForNextNode?.id;
};