Source: layout-content-page-editor-web/src/main/resources/META-INF/resources/js/components/fragment_entry_link/FragmentEntryLinkList.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 Component from 'metal-component';
import {DragDrop} from 'metal-drag-drop';
import position from 'metal-position';
import Soy from 'metal-soy';
import {Config} from 'metal-state';

import '../floating_toolbar/FloatingToolbar.es';

import './FragmentEntryLink.es';

import './FragmentEntryLinkListRow.es';
import {
	CLEAR_DROP_TARGET,
	MOVE_FRAGMENT_ENTRY_LINK,
	MOVE_ROW,
	UPDATE_DROP_TARGET
} from '../../actions/actions.es';
import getConnectedComponent from '../../store/ConnectedComponent.es';
import {shouldUpdatePureComponent} from '../../utils/FragmentsEditorComponentUtils.es';
import {initializeDragDrop} from '../../utils/FragmentsEditorDragDrop.es';
import {
	moveItem,
	setDraggingItemPosition,
	setIn
} from '../../utils/FragmentsEditorUpdateUtils.es';
import {
	FRAGMENTS_EDITOR_ITEM_BORDERS,
	FRAGMENTS_EDITOR_ITEM_TYPES,
	FRAGMENTS_EDITOR_ROW_TYPES
} from '../../utils/constants';
import templates from './FragmentEntryLinkList.soy';

/**
 * FragmentEntryLinkList
 * @review
 */
class FragmentEntryLinkList extends Component {
	/**
	 * Adds drop target types to state
	 * @param {Object} state
	 * @private
	 * @return {Object}
	 * @static
	 */
	static _addDropTargetItemTypesToState(state) {
		let nextState = state;

		nextState = setIn(
			nextState,
			['dropTargetItemTypes'],
			FRAGMENTS_EDITOR_ITEM_TYPES
		);

		nextState = setIn(
			nextState,
			['fragmentsEditorRowTypes'],
			FRAGMENTS_EDITOR_ROW_TYPES
		);

		return nextState;
	}

	/**
	 * Returns whether a drop is valid or not
	 * @param {Object} eventData
	 * @private
	 * @return {boolean}
	 * @static
	 */
	static _dropValid(eventData) {
		const sourceItemData = FragmentEntryLinkList._getItemData(
			eventData.source.dataset
		);
		const targetItemData = FragmentEntryLinkList._getItemData(
			eventData.target ? eventData.target.dataset : null
		);

		let dropValid = false;

		if (sourceItemData.itemType === FRAGMENTS_EDITOR_ITEM_TYPES.row) {
			dropValid =
				targetItemData.itemType === FRAGMENTS_EDITOR_ITEM_TYPES.row &&
				sourceItemData.itemId !== targetItemData.itemId;
		} else if (
			sourceItemData.itemType === FRAGMENTS_EDITOR_ITEM_TYPES.fragment
		) {
			if (
				sourceItemData.fragmentEntryLinkRowType ===
				FRAGMENTS_EDITOR_ROW_TYPES.sectionRow
			) {
				dropValid =
					targetItemData.itemType &&
					sourceItemData.itemId !== targetItemData.itemId &&
					targetItemData.itemType !==
						FRAGMENTS_EDITOR_ITEM_TYPES.column &&
					targetItemData.itemType !==
						FRAGMENTS_EDITOR_ITEM_TYPES.fragment;
			} else {
				dropValid =
					targetItemData.itemType &&
					sourceItemData.itemId !== targetItemData.itemId;
			}
		}

		return dropValid;
	}

	/**
	 * Get id and type of an item from its dataset
	 * @param {!Object} itemDataset
	 * @private
	 * @return {Object}
	 * @static
	 */
	static _getItemData(itemDataset) {
		let itemData = {};

		if (itemDataset) {
			if ('columnId' in itemDataset) {
				itemData = {
					itemId: itemDataset.columnId,
					itemType: FRAGMENTS_EDITOR_ITEM_TYPES.column
				};
			} else if ('fragmentEntryLinkId' in itemDataset) {
				itemData = {
					fragmentEntryLinkRowType:
						itemDataset.fragmentEntryLinkRowType,
					itemId: itemDataset.fragmentEntryLinkId,
					itemType: FRAGMENTS_EDITOR_ITEM_TYPES.fragment
				};
			} else if ('layoutRowId' in itemDataset) {
				itemData = {
					itemId: itemDataset.layoutRowId,
					itemType: FRAGMENTS_EDITOR_ITEM_TYPES.row
				};
			} else if ('fragmentEmptyList' in itemDataset) {
				itemData = {
					itemType: FRAGMENTS_EDITOR_ITEM_TYPES.fragmentList
				};
			}
		}

		return itemData;
	}

	/**
	 * Checks wether a row is empty or not, sets empty parameter
	 * and returns a new state
	 * @param {Object} _state
	 * @private
	 * @return {Object}
	 * @static
	 */
	static _setEmptyRows(_state) {
		return setIn(
			_state,
			['layoutData', 'structure'],
			_state.layoutData.structure.map(row =>
				setIn(
					row,
					['empty'],
					row.columns.every(
						column => column.fragmentEntryLinkIds.length === 0
					)
				)
			)
		);
	}

	/**
	 * @inheritdoc
	 * @private
	 * @review
	 */
	attached() {
		this._initializeDragAndDrop();
	}

	/**
	 * @inheritdoc
	 * @private
	 * @review
	 */
	disposed() {
		this._dragDrop.dispose();
	}

	/**
	 * @inheritdoc
	 * @private
	 * @return {Object}
	 * @review
	 */
	prepareStateForRender(nextState) {
		let _state = FragmentEntryLinkList._addDropTargetItemTypesToState(
			nextState
		);

		_state = FragmentEntryLinkList._setEmptyRows(_state);

		return _state;
	}

	/**
	 * @inheritdoc
	 * @private
	 * @review
	 */
	rendered() {
		if (
			this.activeItemType === FRAGMENTS_EDITOR_ITEM_TYPES.fragmentList &&
			this.element
		) {
			this.element.focus();
		}
	}

	/**
	 * @inheritdoc
	 * @return {boolean}
	 * @review
	 */
	shouldUpdate(changes) {
		return shouldUpdatePureComponent(changes);
	}

	/**
	 * Handle layoutData changed
	 * @inheritDoc
	 * @review
	 */
	syncLayoutData() {
		this._initializeDragAndDrop();
	}

	/**
	 * Callback that is executed when an item is being dragged.
	 * @param {Object} eventData
	 * @param {MouseEvent} eventData.originalEvent
	 * @private
	 * @review
	 */
	_handleDrag(eventData) {
		setDraggingItemPosition(eventData.originalEvent);

		if (FragmentEntryLinkList._dropValid(eventData)) {
			const mouseY = eventData.originalEvent.clientY;
			const targetItem = eventData.target;
			const targetItemRegion = position.getRegion(targetItem);

			const dropTargetItemData = FragmentEntryLinkList._getItemData(
				targetItem.dataset
			);

			let targetBorder = FRAGMENTS_EDITOR_ITEM_BORDERS.bottom;

			if (
				Math.abs(mouseY - targetItemRegion.top) <=
				Math.abs(mouseY - targetItemRegion.bottom)
			) {
				targetBorder = FRAGMENTS_EDITOR_ITEM_BORDERS.top;
			}

			this.store.dispatch({
				dropTargetBorder: targetBorder,
				dropTargetItemId: dropTargetItemData.itemId,
				dropTargetItemType: dropTargetItemData.itemType,
				type: UPDATE_DROP_TARGET
			});
		}
	}

	/**
	 * Callback that is executed when we leave a drag target.
	 * @private
	 * @review
	 */
	_handleDragEnd() {
		this.store.dispatch({
			type: CLEAR_DROP_TARGET
		});
	}

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

		if (FragmentEntryLinkList._dropValid(data)) {
			requestAnimationFrame(() => {
				this._initializeDragAndDrop();
			});

			const itemData = FragmentEntryLinkList._getItemData(
				data.source.dataset
			);

			let moveItemAction = null;
			let moveItemPayload = null;

			if (itemData.itemType === FRAGMENTS_EDITOR_ITEM_TYPES.row) {
				moveItemAction = MOVE_ROW;
				moveItemPayload = {
					rowId: itemData.itemId,
					targetBorder: this.dropTargetBorder,
					targetItemId: this.dropTargetItemId
				};
			} else if (
				itemData.itemType === FRAGMENTS_EDITOR_ITEM_TYPES.fragment
			) {
				moveItemAction = MOVE_FRAGMENT_ENTRY_LINK;
				moveItemPayload = {
					fragmentEntryLinkId: itemData.itemId,
					fragmentEntryLinkRowType: itemData.fragmentEntryLinkRowType,
					targetBorder: this.dropTargetBorder,
					targetItemId: this.dropTargetItemId,
					targetItemType: this.dropTargetItemType
				};
			}

			moveItem(this.store, moveItemAction, moveItemPayload);
		}
	}

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

		this._dragDrop = initializeDragDrop({
			handles: '.fragments-editor__drag-handler',
			sources:
				'.fragments-editor__drag-source--fragment, .fragments-editor__drag-source--layout',
			targets:
				'.fragments-editor__drop-target--fragment, .fragments-editor__drop-target--layout'
		});

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

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

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

/**
 * State definition.
 * @review
 * @static
 * @type {!Object}
 */
FragmentEntryLinkList.STATE = {
	/**
	 * Internal DragDrop instance.
	 * @default null
	 * @instance
	 * @memberOf FragmentEntryLinkList
	 * @review
	 * @type {object|null}
	 */
	_dragDrop: Config.internal().value(null)
};

const ConnectedFragmentEntryLinkList = getConnectedComponent(
	FragmentEntryLinkList,
	[
		'activeItemId',
		'activeItemType',
		'dropTargetBorder',
		'dropTargetItemId',
		'dropTargetItemType',
		'hoveredItemId',
		'hoveredItemType',
		'layoutData',
		'selectedItems'
	]
);

Soy.register(ConnectedFragmentEntryLinkList, templates);

export {ConnectedFragmentEntryLinkList, FragmentEntryLinkList};
export default ConnectedFragmentEntryLinkList;