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

import '../floating_toolbar/background_color/FloatingToolbarBackgroundColorPanel.es';
import '../floating_toolbar/layout_background_image/FloatingToolbarLayoutBackgroundImagePanel.es';
import '../floating_toolbar/spacing/FloatingToolbarSpacingPanel.es';
import './ColumnOverlayGrid.es';
import './FragmentEntryLink.es';
import FloatingToolbar from '../floating_toolbar/FloatingToolbar.es';
import {
	FLOATING_TOOLBAR_BUTTONS,
	FRAGMENTS_EDITOR_ITEM_TYPES,
	FRAGMENTS_EDITOR_ROW_TYPES
} from '../../utils/constants';
import {getAssetFieldValue} from '../../utils/FragmentsEditorFetchUtils.es';
import getConnectedComponent from '../../store/ConnectedComponent.es';
import {
	getItemMoveDirection,
	getItemPath,
	getRowIndex,
	itemIsInPath,
	editableIsMappedToAssetEntry
} from '../../utils/FragmentsEditorGetUtils.es';
import {
	moveRow,
	removeItem,
	setIn
} from '../../utils/FragmentsEditorUpdateUtils.es';
import {shouldUpdatePureComponent} from '../../utils/FragmentsEditorComponentUtils.es';
import templates from './FragmentEntryLinkListRow.soy';
import {updateRowColumnsAction} from '../../actions/updateRowColumns.es';
import {removeRowAction} from '../../actions/removeRow.es';

/**
 * Creates a Fragment Entry Link List Row component.
 */
class FragmentEntryLinkListRow extends Component {
	/**
	 * @param {object} config
	 * @return {object[]} Floating toolbar buttons
	 */
	static _getFloatingToolbarButtons(config) {
		const buttons = [];

		buttons.push(FLOATING_TOOLBAR_BUTTONS.backgroundColor);

		const layoutBackgroundImageButton = {
			...FLOATING_TOOLBAR_BUTTONS.layoutBackgroundImage
		};

		if (
			config.backgroundImage &&
			(config.backgroundImage.mappedField ||
				config.backgroundImage.fieldId)
		) {
			layoutBackgroundImageButton.cssClass =
				'fragments-editor__floating-toolbar--mapped-field';
		}

		buttons.push(layoutBackgroundImageButton);

		buttons.push(FLOATING_TOOLBAR_BUTTONS.spacing);

		return buttons;
	}

	/**
	 * Checks if the given row should be highlighted
	 * @param {string} dropTargetItemId
	 * @param {string} dropTargetItemType
	 * @param {string} rowId
	 * @param {object} structure
	 * @private
	 * @return {boolean}
	 * @review
	 */
	static _isHighlighted(
		dropTargetItemId,
		dropTargetItemType,
		rowId,
		structure
	) {
		let highlighted = false;

		if (dropTargetItemId) {
			const dropTargetPath = getItemPath(
				dropTargetItemId,
				dropTargetItemType,
				structure
			);

			const rowInDropTargetPath = itemIsInPath(
				dropTargetPath,
				rowId,
				FRAGMENTS_EDITOR_ITEM_TYPES.row
			);

			const row = structure.find(row => row.rowId === rowId);

			const rowIsDropTarget =
				dropTargetItemId === rowId &&
				dropTargetItemType === FRAGMENTS_EDITOR_ITEM_TYPES.row;

			highlighted =
				row.type !== FRAGMENTS_EDITOR_ROW_TYPES.sectionRow &&
				rowInDropTargetPath &&
				!rowIsDropTarget;
		}

		return highlighted;
	}

	/**
	 * @inheritdoc
	 */
	created() {
		this._handleBodyMouseLeave = this._handleBodyMouseLeave.bind(this);
		this._handleBodyMouseMove = this._handleBodyMouseMove.bind(this);
		this._handleBodyMouseUp = this._handleBodyMouseUp.bind(this);

		document.body.addEventListener(
			'mouseleave',
			this._handleBodyMouseLeave
		);
		document.body.addEventListener('mouseup', this._handleBodyMouseUp);
	}

	/**
	 * @inheritdoc
	 */
	disposed() {
		this._disposeFloatingToolbar();

		document.body.removeEventListener(
			'mouseleave',
			this._handleBodyMouseLeave
		);
		document.body.removeEventListener(
			'mousemove',
			this._handleBodyMouseMove
		);
		document.body.removeEventListener('mouseup', this._handleBodyMouseUp);
	}

	/**
	 * @inheritdoc
	 * @param {object} state
	 * @return {object}
	 * @review
	 */
	prepareStateForRender(state) {
		let columnResizerVisible;
		let nextState = state;

		if (
			this.rowId === this.activeItemId &&
			this.activeItemType === FRAGMENTS_EDITOR_ITEM_TYPES.row
		) {
			columnResizerVisible = true;
		}

		nextState = setIn(
			nextState,
			['_backgroundImageValue'],
			this._getBackgroundImageValue()
		);

		nextState = setIn(
			nextState,
			['_columnResizerVisible'],
			columnResizerVisible
		);

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

		nextState = setIn(
			nextState,
			['_highlighted'],
			FragmentEntryLinkListRow._isHighlighted(
				state.dropTargetItemId,
				state.dropTargetItemType,
				state.rowId,
				state.layoutData.structure
			)
		);

		if (nextState._resizing && nextState._resizeRowColumns) {
			nextState = setIn(nextState, ['columns'], state._resizeRowColumns);
		}

		return nextState;
	}

	/**
	 * @inheritdoc
	 */
	rendered() {
		if (
			this.rowId === this.activeItemId &&
			this.activeItemType === FRAGMENTS_EDITOR_ITEM_TYPES.row &&
			!this._resizing &&
			this.row.type !== FRAGMENTS_EDITOR_ROW_TYPES.sectionRow
		) {
			this._createFloatingToolbar();
		} else {
			this._disposeFloatingToolbar();
		}

		if (this._resizing) {
			this.element.focus();
		}
	}

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

	/**
	 * Handle getAssetFieldValueURL changed
	 * @inheritDoc
	 * @review
	 */
	syncGetAssetFieldValueURL() {
		this._updateMappedBackgroundFieldValue();
	}

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

	/**
	 * Clears resizing properties.
	 * @private
	 */
	_clearResizing() {
		document.body.removeEventListener(
			'mousemove',
			this._handleBodyMouseMove
		);

		this._resizeColumnIndex = 0;
		this._resizeHighlightedColumn = null;
		this._resizeInitialPosition = 0;
		this._resizeRowColumns = null;
		this._resizing = false;
	}

	/**
	 * Creates a new instance of the floating toolbar.
	 * @private
	 */
	_createFloatingToolbar() {
		const config = {
			anchorElement: this.element,
			buttons: FragmentEntryLinkListRow._getFloatingToolbarButtons(
				this.row.config
			),
			item: this.row,
			itemId: this.rowId,
			itemType: FRAGMENTS_EDITOR_ITEM_TYPES.row,
			portalElement: document.body,
			store: this.store
		};

		if (this._floatingToolbar) {
			this._floatingToolbar.setState(config);
		} else {
			this._floatingToolbar = new FloatingToolbar(config);
		}
	}

	/**
	 * Disposes of an existing floating toolbar instance.
	 * @private
	 */
	_disposeFloatingToolbar() {
		if (this._floatingToolbar) {
			this._floatingToolbar.dispose();

			this._floatingToolbar = null;
		}
	}

	/**
	 * @private
	 */
	_getBackgroundImageValue() {
		if (this._mappedBackgroundFieldValue) {
			return this._mappedBackgroundFieldValue;
		}

		const {config} = this.row;

		if (!config) {
			return '';
		}

		if (typeof config.backgroundImage === 'string') {
			return config.backgroundImage;
		}

		if (typeof config.backgroundImage === 'object') {
			return config.backgroundImage.url;
		}

		return '';
	}

	/**
	 * Callback executed when a row loses focus.
	 * @private
	 * @review
	 */
	_handleBodyMouseLeave() {
		if (this._resizing) {
			this._clearResizing();
		}
	}

	/**
	 * Callback executed when a row is clicked.
	 * @param {Event} event
	 * @private
	 */
	_handleBodyMouseMove(event) {
		let columnElement = this.refs.resizeColumn;
		let columnIndex = this._resizeColumnIndex;
		let nextColumnElement = this.refs.resizeNextColumn;
		let nextColumnIndex = columnIndex + 1;

		const languageDirection =
			Liferay.Language.direction[Liferay.ThemeDisplay.getLanguageId()];

		if (languageDirection === 'rtl') {
			columnElement = this.refs.resizeNextColumn;
			columnIndex = nextColumnIndex;
			nextColumnElement = this.refs.resizeColumn;
			nextColumnIndex = this._resizeColumnIndex;
		}

		const nextColumnRect = nextColumnElement.getBoundingClientRect();

		const maxPosition = nextColumnRect.left + nextColumnRect.width;
		const minPosition = columnElement.getBoundingClientRect().left;
		const position = Math.max(
			Math.min(event.clientX, maxPosition),
			minPosition
		);

		const column = this._resizeRowColumns[columnIndex];
		const nextColumn = this._resizeRowColumns[nextColumnIndex];

		const maxColumns =
			(parseInt(column.size, 10) || 1) +
			(parseInt(nextColumn.size, 10) || 1) -
			1;

		const columns = Math.max(
			Math.round(
				((position - minPosition) / (maxPosition - minPosition)) *
					maxColumns
			),
			1
		);

		this._resizeRowColumns = setIn(
			this._resizeRowColumns,
			[columnIndex, 'size'],
			columns.toString()
		);

		this._resizeRowColumns = setIn(
			this._resizeRowColumns,
			[nextColumnIndex, 'size'],
			(maxColumns - columns + 1).toString()
		);

		if (languageDirection === 'rtl') {
			this._resizeHighlightedColumn =
				maxColumns -
				this._resizeRowColumns
					.slice(columnIndex)
					.map(column => parseInt(column.size, 10) || 1)
					.reduce((size, columnSize) => size + columnSize, 0);
		} else {
			this._resizeHighlightedColumn =
				this._resizeRowColumns
					.slice(0, nextColumnIndex)
					.map(column => parseInt(column.size, 10) || 1)
					.reduce((size, columnSize) => size + columnSize, 0) - 1;
		}
	}

	/**
	 * Callback executed when the mouse hovers over a row.
	 * @param {Event} event
	 * @private
	 */
	_handleBodyMouseUp() {
		if (this._resizing) {
			this._updateRowColumns(this._resizeRowColumns);

			this._clearResizing();
		}
	}

	/**
	 * @private
	 */
	_handleResizerMouseDown(event) {
		this._resizeColumnIndex = this.row.columns.findIndex(
			column => column.columnId === event.delegateTarget.dataset.columnId
		);

		this._resizeInitialPosition = event.clientX;
		this._resizeRowColumns = this.row.columns;
		this._resizing = true;

		document.body.addEventListener('mousemove', this._handleBodyMouseMove);
	}

	/**
	 * Callback executed when a key is pressed on the focused row.
	 * @private
	 * @param {KeyboardEvent} event
	 */
	_handleRowKeyUp(event) {
		if (
			this.activeItemId === this.rowId &&
			this.activeItemType === FRAGMENTS_EDITOR_ITEM_TYPES.row
		) {
			const direction = getItemMoveDirection(event.keyCode);

			if (direction) {
				moveRow(
					direction,
					getRowIndex(this.layoutData.structure, this.rowId),
					this.store,
					this.layoutData.structure
				);
			}
		}
	}

	/**
	 * Callback executed when the remove row button is clicked.
	 * @param {Event} event
	 * @private
	 */
	_handleRowRemoveButtonClick(event) {
		event.stopPropagation();

		removeItem(this.store, removeRowAction(this.hoveredItemId));
	}

	/**
	 * Updates mapped field value
	 * @private
	 * @review
	 */
	_updateMappedBackgroundFieldValue() {
		if (
			this.getAssetFieldValueURL &&
			this.row.config.backgroundImage &&
			editableIsMappedToAssetEntry(this.row.config.backgroundImage)
		) {
			getAssetFieldValue(
				this.row.config.backgroundImage.classNameId,
				this.row.config.backgroundImage.classPK,
				this.row.config.backgroundImage.fieldId
			).then(response => {
				const {fieldValue} = response;

				if (
					fieldValue &&
					fieldValue.url !== this._mappedBackgroundFieldValue
				) {
					this._mappedBackgroundFieldValue = fieldValue.url;
				}
			});
		} else {
			this._mappedBackgroundFieldValue = null;
		}
	}

	/**
	 * Updates row columns.
	 * @param {Array} columns The row columns to update.
	 * @private
	 */
	_updateRowColumns(columns) {
		this.store.dispatch(updateRowColumnsAction(columns, this.rowId));
	}
}

/**
 * State definition.
 * @static
 * @type {!Object}
 */
FragmentEntryLinkListRow.STATE = {
	/**
	 * Floating toolbar instance for internal use.
	 * @default null
	 * @instance
	 * @memberOf FragmentEntryLinkListRow
	 * @type {object|null}
	 */
	_floatingToolbar: Config.internal().value(null),

	/**
	 * Mapped background field value
	 * @instance
	 * @memberOf FragmentEntryLinkListRow
	 * @private
	 * @review
	 * @type {string}
	 */
	_mappedBackgroundFieldValue: Config.internal().string(),

	/**
	 * Index of the column being resized.
	 * @default 0
	 * @instance
	 * @memberOf FragmentEntryLinkListRow
	 * @private
	 * @type {number}
	 */
	_resizeColumnIndex: Config.internal()
		.number()
		.value(0),

	/**
	 * Index of the column that should be highlighted when resized.
	 * @default null
	 * @instance
	 * @memberOf FragmentEntryLinkListRow
	 * @private
	 * @type {number}
	 */
	_resizeHighlightedColumn: Config.internal()
		.number()
		.value(null),

	/**
	 * Mouse position when the resize is started.
	 * @default 0
	 * @instance
	 * @memberOf FragmentEntryLinkListRow
	 * @private
	 * @type {number}
	 */
	_resizeInitialPosition: Config.internal()
		.number()
		.value(0),

	/**
	 * Copy of row columns for resizing.
	 * @default null
	 * @instance
	 * @memberOf FragmentEntryLinkListRow
	 * @type {Array}
	 */
	_resizeRowColumns: Config.internal()
		.array()
		.value(null),

	/**
	 * If <code>true</code>, the user is resizing a column.
	 * @default false
	 * @instance
	 * @memberOf FragmentEntryLinkListRow

	 * @type {boolean}
	 */
	_resizing: Config.internal()
		.bool()
		.value(false),

	/**
	 * Row.
	 * @default undefined
	 * @instance
	 * @memberof FragmentEntryLinkListRow
	 * @type {object}
	 */
	row: Config.object().required(),

	/**
	 * Row ID.
	 * @default undefined
	 * @instance
	 * @memberof FragmentEntryLinkListRow
	 * @type {string}
	 */
	rowId: Config.string().required()
};

const ConnectedFragmentEntryLinkListRow = getConnectedComponent(
	FragmentEntryLinkListRow,
	[
		'activeItemId',
		'activeItemType',
		'dropTargetBorder',
		'dropTargetItemId',
		'dropTargetItemType',
		'getAssetFieldValueURL',
		'hoveredItemId',
		'hoveredItemType',
		'layoutData',
		'mappingFieldsURL',
		'selectedMappingTypes',
		'spritemap'
	]
);

Soy.register(ConnectedFragmentEntryLinkListRow, templates);

export {ConnectedFragmentEntryLinkListRow, FragmentEntryLinkListRow};
export default FragmentEntryLinkListRow;