Source: data-engine-js-components-web/src/main/resources/META-INF/resources/js/core/hooks/useDrop.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import {DataConverter} from 'data-engine-taglib';
import {useDrop as useDndDrop} from 'react-dnd';

import * as DRAG_TYPES from '../../utils/dragTypes';
import {EVENT_TYPES} from '../actions/eventTypes.es';
import {elementSetAdded} from '../thunks/elementSetAdded.es';
import {useConfig} from './useConfig.es';
import {useForm, useFormState} from './useForm.es';

export const DND_ORIGIN_TYPE = {
	EMPTY: 'empty',
	FIELD: 'field',
};

const DRAG_ELEMENT_SET_ADD = 'elementSet:add';

const isSameIndexes = (target, source) =>
	target.pageIndex === source.pageIndex &&
	target.rowIndex === source.rowIndex &&
	target.columnIndex === source.columnIndex;

/**
 * Checks whether Field is moving into itself. In conventional mode, we would be
 * visiting all the fields within it to check if the target belongs to the Field
 * root (in this case the source), but this can be very slow depending on the
 * depth and it is very expensive in this hot path instead this method implement
 * heuristic algorithm based on two assumptions:
 *
 * - The target indexes are the same as the source indexes
 * - The target depth is greater than the source
 *
 * The indexes are an Array<Object> that contains the index of the Field
 * according to the depth level, the head of the array is the current loc and
 * the tail is the root of the tree. It does not compare all target indexes just
 * up to the same level of depth as the source.
 *
 * [
 *  {pageIndex: 0, rowIndex: 1, columnIndex: 0}, <- Head (deeper)
 *  {pageIndex: 0, rowIndex: 0, columnIndex: 0},
 *  {pageIndex: 0, rowIndex: 0, columnIndex: 0}, <- Tail (root)
 * ]
 *
 * The comparison of the indexes also fails in the first index that is not equal
 * to the source index to fail early.
 */
const isMovingIntoItself = (targetParentLoc, sourceParentLoc) => {
	const targetLocClosestSourceTree = targetParentLoc.slice(
		-sourceParentLoc.length
	);

	return (
		!targetLocClosestSourceTree.some(
			(loc, index) => !isSameIndexes(loc, sourceParentLoc[index])
		) && targetParentLoc.length > sourceParentLoc.length
	);
};

/**
 * Just check if the Field is a FieldGroup before checking if it is moving into
 * itself. The index of the source and target are added to the loc, at the
 * level where `useDrop` is used it is not visible the loc of the Field being
 * rendered.
 */
const isFieldGroupMovingIntoItself = ({
	sourceIndexes,
	sourceParentField,
	targetIndexes,
	targetParentField,
	type,
}) =>
	type === 'fieldset' &&
	isMovingIntoItself(
		[targetIndexes, ...(targetParentField?.loc ?? [])],
		[sourceIndexes, ...(sourceParentField?.loc ?? [])]
	);

const isDroppingFieldGroupIntoField = (targetField, sourceField) =>
	sourceField?.type === 'fieldset' && targetField !== undefined;

/**
 * Determines whether the source Field is being moved into inside a Field
 * where its parent is a FieldGroup with just that element.
 */
const isDroppingFieldIntoSingleField = (origin, targetParentField) =>
	origin === DND_ORIGIN_TYPE.FIELD &&
	targetParentField?.type === 'fieldset' &&
	targetParentField.nestedFields.length === 1;

/**
 * Determines whether are moving the source Field into inside the Field
 * in the same FieldGroup to create another FieldGroup but with only
 * two fields.
 */
const isDroppingFieldIntoFieldAndSameGroup = (
	origin,
	sourceParentField,
	targetParentField
) =>
	origin === DND_ORIGIN_TYPE.FIELD &&
	sourceParentField?.type === 'fieldset' &&
	sourceParentField?.fieldName === targetParentField?.fieldName &&
	targetParentField.nestedFields.length === 2;

const isDroppingFieldIntoFieldset = (sourceField, targetField) =>
	sourceField.fieldName !== targetField?.fieldName &&
	targetField?.type === 'fieldset' &&
	!!targetField?.ddmStructureId;

const isSameField = (targetField, sourceField) =>
	targetField && targetField.fieldName === sourceField.fieldName;

export function useDrop({
	columnIndex,
	field,
	origin,
	pageIndex,
	parentField,
	rowIndex,
}) {
	const {editingLanguageId} = useFormState();
	const {fieldTypes} = useConfig();

	const dispatch = useForm();

	const indexes = {columnIndex, pageIndex, rowIndex};
	const {
		DRAG_DATA_DEFINITION_FIELD_ADD,
		DRAG_FIELD_TYPE_ADD,
		DRAG_FIELD_TYPE_MOVE,
		DRAG_FIELDSET_ADD,
	} = DRAG_TYPES;

	const [{canDrop, overTarget}, drop] = useDndDrop({
		accept: [...Object.values(DRAG_TYPES), DRAG_ELEMENT_SET_ADD],
		canDrop: (item) =>
			!isSameField(field, item.data) &&
			!isDroppingFieldGroupIntoField(field, item.data) &&
			!isDroppingFieldIntoFieldset(item.data, field) &&
			!isFieldGroupMovingIntoItself({
				sourceIndexes: item.sourceIndexes,
				sourceParentField: item.sourceParentField,
				targetIndexes: {
					columnIndex,
					pageIndex,
					rowIndex,
				},
				targetParentField: parentField,
				type: item.data.type,
			}),
		collect: (monitor) => ({
			canDrop: monitor.canDrop(),
			overTarget: monitor.isOver({shallow: true}),
		}),
		drop: ({data, sourceIndexes, sourceParentField, type}, monitor) => {
			if (
				monitor.didDrop() ||
				!monitor.canDrop() ||
				isDroppingFieldIntoSingleField(origin, parentField) ||
				isDroppingFieldIntoFieldAndSameGroup(
					origin,
					sourceParentField,
					parentField
				)
			) {
				return;
			}

			switch (type) {
				case DRAG_FIELD_TYPE_ADD:
					dispatch({
						payload: {
							data: {
								fieldName: field?.fieldName,
								parentFieldName: parentField?.fieldName,
							},
							fieldType: {
								...fieldTypes.find(({name}) => {
									return name === data.name;
								}),
								editable: true,
							},
							indexes,
						},
						type:
							origin === DND_ORIGIN_TYPE.EMPTY
								? EVENT_TYPES.FIELD.ADD
								: EVENT_TYPES.SECTION.ADD,
					});
					break;

				case DRAG_FIELD_TYPE_MOVE:
					dispatch({
						payload: {
							sourceFieldName: data.fieldName,
							sourceFieldPage: sourceIndexes.pageIndex,
							sourceParentField,
							targetFieldName: field?.fieldName,
							targetIndexes: indexes,
							targetParentFieldName: parentField?.fieldName,
						},
						type: EVENT_TYPES.DND.MOVE,
					});
					break;

				case DRAG_DATA_DEFINITION_FIELD_ADD: {
					const {dataDefinition, name} = data;

					const {
						fieldType,
						label,
						settingsContext,
					} = DataConverter.getDataDefinitionFieldByFieldName({
						dataDefinition,
						editingLanguageId,
						fieldName: name,
						fieldTypes,
					});

					dispatch({
						payload: {
							data: {
								fieldName: field?.fieldName,
								parentFieldName: parentField?.fieldName,
							},
							fieldType: {
								...fieldTypes.find(({name}) => {
									return name === fieldType;
								}),
								editable: true,
								label:
									label[
										editingLanguageId ??
											themeDisplay.getLanguageId()
									],
								settingsContext,
							},
							indexes,
							skipFieldNameGeneration: true,
						},
						type:
							origin === DND_ORIGIN_TYPE.EMPTY
								? EVENT_TYPES.FIELD.ADD
								: EVENT_TYPES.SECTION.ADD,
					});
					break;
				}
				case DRAG_FIELDSET_ADD: {
					const {fieldSet, properties, useFieldName} = data;

					dispatch({
						payload: {
							fieldName: field?.fieldName,
							fieldSet,
							indexes,
							parentFieldName: parentField?.fieldName,
							properties,
							useFieldName,
						},
						type: EVENT_TYPES.FIELD_SET.ADD,
					});
					break;
				}

				case DRAG_ELEMENT_SET_ADD:
					dispatch(elementSetAdded({indexes, ...data.payload}));
					break;

				default:
					break;
			}
		},
	});

	return {
		canDrop,
		drop,
		overTarget,
	};
}