Source: layout-admin-web/src/main/resources/META-INF/resources/js/miller_columns/utils/LayoutDragDrop.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 {Drag, DragDrop} from 'metal-drag-drop';
import position from 'metal-position';
import State, {Config} from 'metal-state';

/**
 * Positions where elements can be dragged to
 * @review
 */

const DROP_TARGET_BORDERS = {
	bottom: 'layout-column-item-drag-bottom',
	inside: 'layout-column-item-drag-inside',
	top: 'layout-column-item-drag-top'
};

/**
 * Possible drop target types
 * @review
 */

const DROP_TARGET_ITEM_TYPES = {
	column: 'layout-column',
	item: 'layout-column-item'
};

/**
 * LayoutDragDrop
 */
class LayoutDragDrop extends State {
	/**
	 * @inheritDoc
	 * @review
	 */
	constructor(config, ...args) {
		super(config, ...args);

		this._initializeDragAndDrop();
	}

	/**
	 * @inheritDoc
	 * @review
	 */
	dispose() {
		this._dragDrop.dispose();

		super.dispose();
	}

	/**
	 * Adds a target to the list
	 * @param {!HTMLElement} target
	 * @review
	 */
	addTarget(target) {
		this._dragDrop.addTarget(target);
	}

	/**
	 * Callback that is executed when an item is being dragged.
	 * @param {!object} data
	 * @param {!MouseEvent} data.originalEvent
	 * @param {!HTMLElement} data.target
	 * @private
	 * @review
	 */
	_handleDrag(data) {
		const targetItem = data.target;

		if (targetItem) {
			const mouseY = data.originalEvent
				? data.originalEvent.clientY
				: data.relativeY;

			const placeholderItemRegion = position.getRegion(data.placeholder);
			const sourceItemPlid = data.source.dataset.layoutColumnItemPlid;
			const targetItemRegion = position.getRegion(targetItem);

			let targetId = null;
			let targetType = null;

			if (targetItem.dataset.layoutColumnIndex) {
				targetId = targetItem.dataset.layoutColumnIndex;
				targetId = targetId === '0' ? null : targetId;
				targetType = DROP_TARGET_ITEM_TYPES.column;
			} else if (targetItem.dataset.layoutColumnItemPlid) {
				targetId = targetItem.dataset.layoutColumnItemPlid;
				targetType = DROP_TARGET_ITEM_TYPES.item;

				if (
					placeholderItemRegion.top > targetItemRegion.top &&
					placeholderItemRegion.bottom < targetItemRegion.bottom
				) {
					this._draggingItemPosition = DROP_TARGET_BORDERS.inside;
				} else if (
					Math.abs(mouseY - targetItemRegion.top) <=
					Math.abs(mouseY - targetItemRegion.bottom)
				) {
					this._draggingItemPosition = DROP_TARGET_BORDERS.top;
				} else {
					this._draggingItemPosition = DROP_TARGET_BORDERS.bottom;
				}
			}

			this.emit('dragLayoutColumnItem', {
				position: this._draggingItemPosition,
				sourceItemPlid,
				targetId,
				targetType
			});
		}
	}

	/**
	 * Callback that is executed when a target is leaved.
	 * @private
	 * @review
	 */
	_handleDragEnd() {
		this.emit('leaveLayoutColumnItem');
	}

	/**
	 * Callback that is executed when a target starts being dragged.
	 * @param {object} data
	 * @param {MouseEvent} event
	 * @private
	 * @review
	 */
	_handleDragStart(data, event) {
		const sourceItemPlid = event.target.getActiveDrag().dataset
			.layoutColumnItemPlid;

		this.emit('startMovingLayoutColumnItem', {
			sourceItemPlid
		});
	}

	/**
	 * Callback that is executed when an item is dropped.
	 * @param {!object} data
	 * @param {!HTMLElement} data.source
	 * @param {HTMLElement} data.target
	 * @param {!MouseEvent} event
	 * @private
	 * @review
	 */
	_handleDrop(data, event) {
		event.preventDefault();

		const sourceItemPlid = data.source.dataset.layoutColumnItemPlid;
		let targetId = null;
		let targetType = null;

		if (data.target) {
			if (data.target.dataset.layoutColumnIndex) {
				targetId = data.target.dataset.layoutColumnIndex;
				targetId = targetId === '0' ? null : targetId;
				targetType = DROP_TARGET_ITEM_TYPES.column;
			} else if (data.target.dataset.layoutColumnItemPlid) {
				targetId = data.target.dataset.layoutColumnItemPlid;
				targetType = DROP_TARGET_ITEM_TYPES.item;
			}
		}

		this.emit('dropLayoutColumnItem', {
			sourceItemPlid,
			targetId,
			targetType
		});
	}

	/**
	 * @private
	 * @review
	 */
	_initializeDragAndDrop() {
		if (this._dragDrop) {
			this._dragDrop.dispose();
		}

		this._dragDrop = new DragDrop({
			autoScroll: true,
			dragPlaceholder: Drag.Placeholder.CLONE,
			handles: '.layout-drag-handler',
			scrollContainers: '.layout-column, .layout-columns',
			sources: '.layout-drag-item',
			targets: '.layout-drop-target-item'
		});

		this._dragDrop.on(DragDrop.Events.DRAG, this._handleDrag.bind(this));

		this._dragDrop.on(DragDrop.Events.END, this._handleDrop.bind(this));

		this._dragDrop.on(Drag.Events.START, this._handleDragStart.bind(this));

		this._dragDrop.on(
			DragDrop.Events.TARGET_LEAVE,
			this._handleDragEnd.bind(this)
		);
	}
}

/**
 * State definition.
 * @type {!Object}
 * @static
 */

LayoutDragDrop.STATE = {
	/**
	 * Internal DragDrop instance.
	 * @default null
	 * @instance
	 * @memberOf LayoutDragDrop
	 * @review
	 * @type {object|null}
	 */

	_dragDrop: Config.internal().value(null),

	/**
	 * Position of the dragging card
	 * @default undefined
	 * @instance
	 * @memberOf LayoutDragDrop
	 * @review
	 * @type {!string}
	 */

	_draggingItemPosition: Config.internal().string()
};

export {DROP_TARGET_BORDERS, DROP_TARGET_ITEM_TYPES, LayoutDragDrop};
export default LayoutDragDrop;