Source: layout-content-page-editor-web/src/main/resources/META-INF/resources/js/components/fragment_entry_link/FragmentEntryLink.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/fragment_configuration/FloatingToolbarFragmentConfigurationPanel.es';

import './FragmentEntryLinkContent.es';
import {
	MOVE_FRAGMENT_ENTRY_LINK,
	UPDATE_SELECTED_SIDEBAR_PANEL_ID
} from '../../actions/actions.es';
import {duplicateFragmentEntryLinkAction} from '../../actions/duplicateFragmentEntryLink.es';
import {removeFragmentEntryLinkAction} from '../../actions/removeFragmentEntryLinks.es';
import {
	disableSavingChangesStatusAction,
	enableSavingChangesStatusAction,
	updateLastSaveDateAction
} from '../../actions/saveChanges.es';
import {updateActiveItemAction} from '../../actions/updateActiveItem.es';
import {getConnectedComponent} from '../../store/ConnectedComponent.es';
import {shouldUpdatePureComponent} from '../../utils/FragmentsEditorComponentUtils.es';
import {
	getFragmentColumn,
	getFragmentRowIndex,
	getItemMoveDirection,
	getItemPath,
	getTargetBorder,
	getWidget,
	itemIsInPath
} from '../../utils/FragmentsEditorGetUtils.es';
import {
	moveItem,
	moveRow,
	removeItem
} from '../../utils/FragmentsEditorUpdateUtils.es';
import {
	FLOATING_TOOLBAR_BUTTONS,
	FRAGMENTS_EDITOR_ITEM_TYPES,
	FRAGMENTS_EDITOR_ROW_TYPES,
	FREEMARKER_FRAGMENT_ENTRY_PROCESSOR
} from '../../utils/constants';
import {prefixSegmentsExperienceId} from '../../utils/prefixSegmentsExperienceId.es';
import FloatingToolbar from '../floating_toolbar/FloatingToolbar.es';
import templates from './FragmentEntryLink.soy';

/**
 * FragmentEntryLink
 * @review
 */
class FragmentEntryLink extends Component {
	/**
	 * @inheritdoc
	 */
	created() {
		this._handleFloatingToolbarButtonClicked = this._handleFloatingToolbarButtonClicked.bind(
			this
		);
	}

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

	/**
	 * @inheritdoc
	 * @param {object} state
	 * @return {object}
	 * @review
	 */
	prepareStateForRender(state) {
		const hoveredPath = getItemPath(
			state.hoveredItemId,
			state.hoveredItemType,
			state.layoutData.structure
		);

		return {
			...state,

			_fragmentEntryLinkRowType: state.rowType,
			_fragmentsEditorItemTypes: FRAGMENTS_EDITOR_ITEM_TYPES,
			_fragmentsEditorRowTypes: FRAGMENTS_EDITOR_ROW_TYPES,

			_hovered: itemIsInPath(
				hoveredPath,
				state.fragmentEntryLinkId,
				FRAGMENTS_EDITOR_ITEM_TYPES.fragment
			),

			_showComments: state.sidebarPanels.some(
				sidebarPanel => sidebarPanel.sidebarPanelId === 'comments'
			)
		};
	}

	/**
	 * @inheritdoc
	 */
	rendered() {
		if (
			this.fragmentEntryLinkId === this.activeItemId &&
			this.activeItemType === FRAGMENTS_EDITOR_ITEM_TYPES.fragment
		) {
			this._createFloatingToolbar();
		} else {
			this._disposeFloatingToolbar();
		}
	}

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

	/**
	 * @inheritDoc
	 * @review
	 */
	syncFragmentEntryLinks() {
		if (this.fragmentEntryLinks[this.fragmentEntryLinkId]) {
			const defaultSegmentsExperienceId = prefixSegmentsExperienceId(
				this.defaultSegmentsExperienceId
			);
			const segmentsExperienceId = prefixSegmentsExperienceId(
				this.segmentsExperienceId
			);

			const configurationValues = this.fragmentEntryLinks[
				this.fragmentEntryLinkId
			].editableValues[FREEMARKER_FRAGMENT_ENTRY_PROCESSOR];

			this._configuration = this.fragmentEntryLinks[
				this.fragmentEntryLinkId
			].configuration;

			this._defaultConfigurationValues = this.fragmentEntryLinks[
				this.fragmentEntryLinkId
			].defaultConfigurationValues;

			if (configurationValues) {
				const segmentedConfigurationValues =
					configurationValues[segmentsExperienceId] ||
					configurationValues[defaultSegmentsExperienceId] ||
					configurationValues;

				this._configurationValues = {
					...this._defaultConfigurationValues,
					...segmentedConfigurationValues
				};
			}
		}
	}

	/**
	 * Creates a new instance of the floating toolbar.
	 * @private
	 */
	_createFloatingToolbar() {
		const config = {
			anchorElement: this.element,
			buttons: this._getFloatingToolbarButtons(),
			events: {
				buttonClicked: this._handleFloatingToolbarButtonClicked
			},
			item: {
				configuration: this._configuration,
				configurationValues: this._configurationValues,
				defaultConfigurationValues: this._defaultConfigurationValues,
				fragmentEntryLinkId: this.fragmentEntryLinkId
			},
			itemId: this.fragmentEntryLinkId,
			itemType: FRAGMENTS_EDITOR_ITEM_TYPES.fragment,
			portalElement: document.body,
			store: this.store
		};

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

	/**
	 * Duplicate this fragmentEntryLink
	 * @private
	 */
	_duplicateFragmentEntryLink() {
		this.store
			.dispatch(enableSavingChangesStatusAction())
			.dispatch(
				duplicateFragmentEntryLinkAction(
					this.fragmentEntryLinkId,
					this.rowType
				)
			)
			.dispatch(updateLastSaveDateAction())
			.dispatch(disableSavingChangesStatusAction());
	}

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

			this._floatingToolbar = null;
		}
	}

	/**
	 * @private
	 * @return {object[]} Floating toolbar buttons
	 * @review
	 */
	_getFloatingToolbarButtons() {
		const buttons = [];

		const fragmentEntryLink = this.fragmentEntryLinks[
			this.fragmentEntryLinkId
		];

		const widget =
			fragmentEntryLink.portletId &&
			getWidget(this.widgets, fragmentEntryLink.portletId);

		if (!widget || widget.instanceable) {
			buttons.push(FLOATING_TOOLBAR_BUTTONS.duplicateFragment);
		}

		if (this._shouldShowConfigPanel()) {
			buttons.push(FLOATING_TOOLBAR_BUTTONS.fragmentConfiguration);
		}

		return buttons;
	}

	/**
	 * Callback executed when an floating toolbar button is clicked
	 * @param {Event} event
	 * @param {Object} data
	 * @private
	 */
	_handleFloatingToolbarButtonClicked(event, data) {
		const {panelId} = data;

		if (panelId === FLOATING_TOOLBAR_BUTTONS.duplicateFragment.panelId) {
			event.preventDefault();

			this._duplicateFragmentEntryLink();
		}
	}

	/**
	 * Handle fragment keyup event so it can emit when it
	 * should be moved or selected.
	 * @param {KeyboardEvent} event
	 * @private
	 * @review
	 */
	_handleFragmentKeyUp(event) {
		event.stopPropagation();

		const direction = getItemMoveDirection(event.keyCode);
		const {fragmentEntryLinkRowType} = event.delegateTarget.dataset;

		if (direction) {
			if (
				fragmentEntryLinkRowType ===
				FRAGMENTS_EDITOR_ROW_TYPES.sectionRow
			) {
				moveRow(
					direction,
					getFragmentRowIndex(
						this.layoutData.structure,
						this.fragmentEntryLinkId
					),
					this.store,
					this.layoutData.structure
				);
			} else {
				const column = getFragmentColumn(
					this.layoutData.structure,
					this.fragmentEntryLinkId
				);
				const fragmentIndex = column.fragmentEntryLinkIds.indexOf(
					this.fragmentEntryLinkId
				);
				const targetFragmentEntryLinkId =
					column.fragmentEntryLinkIds[fragmentIndex + direction];

				if (direction && targetFragmentEntryLinkId) {
					const moveItemPayload = {
						fragmentEntryLinkId: this.fragmentEntryLinkId,
						targetBorder: getTargetBorder(direction),
						targetItemId: targetFragmentEntryLinkId,
						targetItemType: FRAGMENTS_EDITOR_ITEM_TYPES.fragment
					};

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

	/**
	 * @private
	 * @review
	 */
	_handleFragmentCommentsButtonClick() {
		this.store.dispatch(
			updateActiveItemAction(
				this.fragmentEntryLinkId,
				FRAGMENTS_EDITOR_ITEM_TYPES.fragment
			)
		);

		this.store.dispatch({
			type: UPDATE_SELECTED_SIDEBAR_PANEL_ID,
			value: 'comments'
		});
	}

	/**
	 * Callback executed when the fragment remove button is clicked.
	 * @param {Object} event
	 * @private
	 */
	_handleFragmentRemoveButtonClick(event) {
		event.stopPropagation();

		removeItem(
			this.store,
			removeFragmentEntryLinkAction(this.fragmentEntryLinkId)
		);
	}

	/**
	 * Returns wether the config panel should be shown or not
	 * @private
	 * @review
	 */
	_shouldShowConfigPanel() {
		const fieldSetsExist =
			this._configuration &&
			Array.isArray(this._configuration.fieldSets) &&
			this._configuration.fieldSets.length > 0;

		const fragmentIsActive =
			this.fragmentEntryLinkId === this.activeItemId &&
			this.activeItemType === FRAGMENTS_EDITOR_ITEM_TYPES.fragment;

		return fieldSetsExist && fragmentIsActive;
	}
}

/**
 * State definition.
 * @review
 * @static
 * @type {!Object}
 */
FragmentEntryLink.STATE = {
	/**
	 * Fragment Entry Configuration
	 * @instance
	 * @memberOf FragmentEntryLink
	 * @type {object}
	 */
	_configuration: Config.object().internal(),

	/**
	 * Fragment Entry Link Configuration values
	 * @instance
	 * @memberOf FragmentEntryLink
	 * @type {object}
	 */
	_configurationValues: Config.object().internal(),

	/**
	 * Fragment Entry Link Default configuration values
	 * @instance
	 * @memberOf FragmentEntryLink
	 * @type {object}
	 */
	_defaultConfigurationValues: Config.object().internal(),

	/**
	 * Floating toolbar instance for internal use.
	 * @default null
	 * @instance
	 * @memberOf FragmentEntryLink
	 * @type {object|null}
	 */
	_floatingToolbar: Config.internal().value(null),

	/**
	 * FragmentEntryLink id
	 * @default undefined
	 * @instance
	 * @memberOf FragmentEntryLink
	 * @review
	 * @type {!string}
	 */
	fragmentEntryLinkId: Config.string().required(),

	/**
	 * Fragment name
	 * @default ''
	 * @instance
	 * @memberOf FragmentEntryLink
	 * @review
	 * @type {string}
	 */
	name: Config.string().value(''),

	/**
	 * Row type
	 * @instance
	 * @memberOf FragmentEntryLink
	 * @review
	 * @type {string}
	 */
	rowType: Config.string(),

	/**
	 * Shows FragmentEntryLink control toolbar
	 * @default true
	 * @instance
	 * @memberOf FragmentEntryLink
	 * @review
	 * @type {!bool}
	 */
	showControlBar: Config.bool().value(true),

	/**
	 * CSS class to modify style
	 * @default undefined
	 * @instance
	 * @memberOf FragmentEntryLink
	 * @review
	 * @type {!string}
	 */
	styleModifier: Config.string()
};

const ConnectedFragmentEntryLink = getConnectedComponent(FragmentEntryLink, [
	'activeItemId',
	'activeItemType',
	'defaultLanguageId',
	'defaultSegmentsExperienceId',
	'dropTargetItemId',
	'dropTargetItemType',
	'dropTargetBorder',
	'duplicateFragmentEntryLinkURL',
	'fragmentEntryLinks',
	'hoveredItemId',
	'hoveredItemType',
	'imageSelectorURL',
	'languageId',
	'layoutData',
	'portletNamespace',
	'segmentsExperienceId',
	'selectedMappingTypes',
	'selectedSidebarPanelId',
	'sidebarPanels',
	'spritemap',
	'widgets'
]);

Soy.register(ConnectedFragmentEntryLink, templates);

export {ConnectedFragmentEntryLink, FragmentEntryLink};

export default ConnectedFragmentEntryLink;