All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.resources.js.components.keyboard_movement.KeyboardMovementManager.ts Maven / Gradle / Ivy

The newest version!
/**
 * SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com
 * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06
 */

import {useEventListener} from '@liferay/frontend-js-react-web';
import {sub} from 'frontend-js-web';
import {useEffect} from 'react';

import {Criteria} from '../../../types/Criteria';
import {
	POSITIONS,
	Source,
	Target,
	useDisableKeyboardMovement,
	useMovementSource,
	useMovementTarget,
	useSendMovementMessage,
	useSetMovementTarget,
} from '../../contexts/KeyboardMovementContext';
import {KeyboardKey} from '../../types/KeyboardKey';
import dropInGroup from '../../utils/dropInGroup';
import dropInRow from '../../utils/dropInRow';
import getGroup from '../../utils/getGroup';

export const ACTION_TYPES = {
	add: 'add',
	move: 'move',
} as const;

const DIRECTIONS = {
	down: 'down',
	up: 'up',
} as const;

type Direction = (typeof DIRECTIONS)[keyof typeof DIRECTIONS];

type Contributor = {
	criteriaMap: Criteria;
	propertyKey: string;
};

type Props = {
	contributors: Contributor[];
	onMove: (criteria: Criteria, propertyKey: PropertyKey) => void;
};

export default function KeyboardMovementManager({contributors, onMove}: Props) {
	const source = useMovementSource()!;
	const target = useMovementTarget();
	const sendMessage = useSendMovementMessage();
	const setTarget = useSetMovementTarget();
	const disableMovement = useDisableKeyboardMovement();

	const criteria = getCriteria(contributors, source.propertyKey);

	useEventListener(
		'keydown',
		(event) => {
			event.stopPropagation();
			event.preventDefault();

			const key = (event).key as KeyboardKey;

			// Move down

			if (key === 'ArrowDown') {
				const nextTarget = getNextTarget(
					source,
					target!,
					DIRECTIONS.down,
					criteria!
				);

				if (nextTarget) {
					setTarget(nextTarget);

					sendMessage(
						sub(
							Liferay.Language.get(
								'targeting-x-of-item-x-of-group-x'
							),
							nextTarget.position,
							nextTarget.index.toString(),
							getGroupNumber(nextTarget.groupId)!
						)
					);
				}
			}

			// Move up

			else if (key === 'ArrowUp') {
				const nextTarget = getNextTarget(
					source,
					target!,
					DIRECTIONS.up,
					criteria!
				);

				if (nextTarget) {
					setTarget(nextTarget);

					sendMessage(
						sub(
							Liferay.Language.get(
								'targeting-x-of-item-x-of-group-x'
							),
							nextTarget.position,
							nextTarget.index.toString(),
							getGroupNumber(nextTarget.groupId)!
						)
					);
				}
			}

			// Move to end

			else if (key === 'End') {
				const nextTarget = {
					groupId: criteria!.groupId,
					index: criteria!.items.length - 1,
					position: POSITIONS.bottom,
				};

				if (nextTarget) {
					setTarget(nextTarget);

					sendMessage(
						sub(
							Liferay.Language.get(
								'targeting-x-of-item-x-of-group-x'
							),
							nextTarget.position,
							nextTarget.index.toString(),
							getGroupNumber(nextTarget.groupId)!
						)
					);
				}
			}

			// Execute action

			else if (key === 'Enter') {
				let nextCriteria;

				if (target!.position === POSITIONS.middle) {
					nextCriteria = dropInRow(criteria!, source, target!);
				}
				else {
					nextCriteria = dropInGroup(criteria!, source, target!);
				}

				onMove(nextCriteria, source.propertyKey);

				sendMessage(
					sub(
						Liferay.Language.get(
							'x-placed-on-x-of-item-x-of-group-x'
						),
						source.label,
						target!.position,
						target!.index.toString(),
						getGroupNumber(target!.groupId)!
					)
				);

				disableMovement();
			}

			// Disable movement

			else if (key === 'Escape') {
				disableMovement();
			}

			// Move to start

			else if (key === 'Home') {
				const nextTarget = {
					groupId: criteria!.groupId,
					index: 0,
					position: POSITIONS.top,
				};

				setTarget(nextTarget);

				sendMessage(
					sub(
						Liferay.Language.get(
							'targeting-x-of-item-x-of-group-x'
						),
						nextTarget.position,
						nextTarget.index.toString(),
						getGroupNumber(nextTarget.groupId)!
					)
				);
			}
		},
		true,
		document
	);

	useEffect(() => {
		if (target) {
			return;
		}

		const initialTarget = getInitialTarget(source, criteria)!;

		if (isValidTarget(initialTarget, source)) {
			setTarget(initialTarget);

			sendMessage(
				sub(
					Liferay.Language.get(
						'use-up-and-down-arrows-to-move-the-item-and-press-enter-to-place-it-in-desired-position.-currently-targeting-x-of-item-x-of-group-x'
					),
					initialTarget.position,
					initialTarget.index.toString(),
					getGroupNumber(initialTarget.groupId)!
				)
			);
		}
		else {
			disableMovement();
		}
	}, [criteria, disableMovement, sendMessage, setTarget, source, target]);

	return null;
}

function getCriteria(
	contributors: Contributor[],
	propertyKey: Source['propertyKey']
) {
	return (
		contributors.find(
			(contributor) => contributor.propertyKey === propertyKey
		)?.criteriaMap ?? null
	);
}

function getParentGroup(
	groupId: Criteria['groupId'],
	criteria: Criteria
): Criteria | undefined {
	for (const child of criteria.items) {
		if ('groupId' in child) {
			if (child.groupId === groupId) {
				return criteria;
			}

			const parentGroup = getParentGroup(groupId, child);

			if (parentGroup) {
				return parentGroup;
			}
		}
	}
}

function getGroupNumber(groupId: Criteria['groupId']) {
	return groupId.split('_').pop();
}

function isValidTarget(target: Target, source: Source) {
	return !(
		!target ||
		(target.groupId === source.groupId && target.index === source.index)
	);
}

function getInitialTarget(source: Source, criteria: Criteria | null) {
	const actionType = source.groupId ? ACTION_TYPES.move : ACTION_TYPES.add;

	if (actionType === ACTION_TYPES.add) {
		if (criteria) {
			return {
				groupId: criteria.groupId,
				index: criteria.items.length - 1,
				position: POSITIONS.bottom,
			};
		}
		else {
			return {
				groupId: 'root',
				index: 0,
				position: POSITIONS.middle,
			};
		}
	}
	else if (actionType === ACTION_TYPES.move) {
		return (
			getNextTarget(
				source,
				{
					groupId: source.groupId!,
					index: source.index!,
					position: POSITIONS.middle,
				},
				DIRECTIONS.down,
				criteria!
			) ||
			getNextTarget(
				source,
				{
					groupId: source.groupId!,
					index: source.index!,
					position: POSITIONS.middle,
				},
				DIRECTIONS.up,
				criteria!
			)
		);
	}
}

function getNextTarget(
	source: Source,
	target: Target,
	direction: Direction,
	criteria: Criteria
): Target | null {
	const {groupId, index, position} = target;

	const group = getGroup(groupId, criteria)!;

	const item = group.items[index];

	let nextTarget = target;

	if (direction === DIRECTIONS.up) {
		if (position === POSITIONS.bottom) {
			if ('groupId' in item) {
				nextTarget = {
					groupId: item.groupId,
					index: item.items.length - 1,
					position: POSITIONS.bottom,
				};
			}
			else {
				nextTarget = {
					groupId,
					index,
					position: POSITIONS.middle,
				};
			}
		}
		else if (position === POSITIONS.middle) {
			nextTarget = {
				groupId,
				index,
				position: POSITIONS.top,
			};
		}
		else if (position === POSITIONS.top) {
			if (index === 0) {
				const parentGroup = getParentGroup(groupId, criteria);

				if (!parentGroup) {
					return null;
				}
				else if (parentGroup.items.indexOf(group) === 0) {
					nextTarget = {
						groupId: parentGroup.groupId,
						index: 0,
						position: POSITIONS.top,
					};
				}
				else {
					nextTarget = {
						groupId: parentGroup.groupId,
						index: parentGroup.items.indexOf(group),
						position: POSITIONS.top,
					};
				}
			}
			else {
				nextTarget = {
					groupId,
					index: index - 1,
					position: POSITIONS.bottom,
				};
			}
		}
	}
	else if (direction === DIRECTIONS.down) {
		if (position === POSITIONS.top) {
			if ('groupId' in item) {
				nextTarget = {
					groupId: item.groupId,
					index: 0,
					position: POSITIONS.top,
				};
			}
			else {
				nextTarget = {
					groupId,
					index,
					position: POSITIONS.middle,
				};
			}
		}
		else if (position === POSITIONS.middle) {
			nextTarget = {
				groupId,
				index,
				position: POSITIONS.bottom,
			};
		}
		else if (position === POSITIONS.bottom) {
			if (index === group.items.length - 1) {
				const parentGroup = getParentGroup(groupId, criteria);

				if (!parentGroup) {
					return null;
				}
				else if (
					parentGroup.items.indexOf(group) ===
					parentGroup.items.length - 1
				) {
					nextTarget = {
						groupId: parentGroup.groupId,
						index: parentGroup.items.length - 1,
						position: POSITIONS.bottom,
					};
				}
				else {
					nextTarget = {
						groupId: parentGroup.groupId,
						index: parentGroup.items.indexOf(group),
						position: POSITIONS.bottom,
					};
				}
			}
			else {
				nextTarget = {
					groupId,
					index: index + 1,
					position: POSITIONS.top,
				};
			}
		}
	}

	if (isValidTarget(nextTarget!, source)) {
		return nextTarget;
	}

	return getNextTarget(source, nextTarget, direction, criteria);
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy