Source: dynamic-data-mapping-form-builder/src/main/resources/META-INF/resources/js/components/Sidebar/Sidebar.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 classnames from 'classnames';
import ClayButton from 'clay-button';
import {ClayActionsDropdown, ClayDropdownBase} from 'clay-dropdown';
import {ClayIcon} from 'clay-icon';
import ClayModal from 'clay-modal';
import {
	Form,
	FormSupport,
	PagesVisitor,
	generateInstanceId,
	generateName,
} from 'dynamic-data-mapping-form-renderer';
import {makeFetch} from 'dynamic-data-mapping-form-renderer/js/util/fetch.es';
import dom from 'metal-dom';
import {Drag, DragDrop} from 'metal-drag-drop';
import {EventHandler} from 'metal-events';
import Component, {Fragment} from 'metal-jsx';
import {Config} from 'metal-state';

import {focusedFieldStructure} from '../../util/config.es';
import {selectText} from '../../util/dom.es';
import {
	getFieldProperties,
	normalizeSettingsContextPages,
} from '../../util/fieldSupport.es';
import FieldTypeBox from '../FieldTypeBox/FieldTypeBox.es';

/**
 * Sidebar is a tooling to mount forms.
 */

class Sidebar extends Component {
	attached() {
		this._bindDragAndDrop();

		this._eventHandler.add(
			dom.on(document, 'mousedown', this._handleDocumentMouseDown, false)
		);
	}

	changeFieldType(type) {
		const {
			defaultLanguageId,
			editingLanguageId,
			fieldTypes,
			focusedField,
		} = this.props;
		const {dispatch} = this.context;
		const newFieldType = fieldTypes.find(({name}) => name === type);
		const newSettingsContext = {
			...newFieldType.settingsContext,
			pages: normalizeSettingsContextPages(
				newFieldType.settingsContext.pages,
				defaultLanguageId,
				editingLanguageId,
				newFieldType,
				focusedField.fieldName
			),
		};
		let {settingsContext} = focusedField;

		if (type !== focusedField.type) {
			settingsContext = this._mergeFieldTypeSettings(
				settingsContext,
				newSettingsContext
			);
		}

		dispatch('focusedFieldEvaluationEnded', {
			...focusedField,
			...newFieldType,
			...getFieldProperties(
				settingsContext,
				defaultLanguageId,
				editingLanguageId
			),
			changedFieldType: true,
			instanceId: generateInstanceId(8),
			settingsContext,
			type: newFieldType.name,
		});
	}

	close() {
		this.setState({
			open: false,
		});
	}

	created() {
		this._eventHandler = new EventHandler();
		const transitionEnd = this._getTransitionEndEvent();

		this.supportsTransitionEnd = transitionEnd !== false;
		this.transitionEnd = transitionEnd || 'transitionend';

		this._handleChangeFieldTypeItemClicked = this._handleChangeFieldTypeItemClicked.bind(
			this
		);
		this._handleCloseButtonClicked = this._handleCloseButtonClicked.bind(
			this
		);
		this._handleDocumentMouseDown = this._handleDocumentMouseDown.bind(
			this
		);
		this._handleDragEnded = this._handleDragEnded.bind(this);
		this._handleDragStarted = this._handleDragStarted.bind(this);
		this._handleDragTargetEnter = this._handleDragTargetEnter.bind(this);
		this._handleDragTargetLeave = this._handleDragTargetLeave.bind(this);
		this._handleEvaluatorChanged = this._handleEvaluatorChanged.bind(this);
		this._handleElementSettingsClicked = this._handleElementSettingsClicked.bind(
			this
		);
		this._handlePreviousButtonClicked = this._handlePreviousButtonClicked.bind(
			this
		);
		this._handleSettingsFieldBlurred = this._handleSettingsFieldBlurred.bind(
			this
		);
		this._handleSettingsFieldEdited = this._handleSettingsFieldEdited.bind(
			this
		);
		this._handleSettingsFormAttached = this._handleSettingsFormAttached.bind(
			this
		);
		this._handleTabItemClicked = this._handleTabItemClicked.bind(this);
		this._renderFieldTypeDropdownLabel = this._renderFieldTypeDropdownLabel.bind(
			this
		);
	}

	disposeDragAndDrop() {
		if (this._dragAndDrop) {
			this._dragAndDrop.dispose();
		}
	}

	disposeInternal() {
		super.disposeInternal();

		this._eventHandler.removeAllListeners();
		this.disposeDragAndDrop();
	}

	getDropTargetsSelector() {
		return '.ddm-target:not([data-drop-disabled="true"])';
	}

	getSettingsFormContext() {
		const {
			defaultLanguageId,
			editingLanguageId,
			focusedField,
			readOnlyFieldName,
		} = this.props;
		const {settingsContext} = focusedField;
		const visitor = new PagesVisitor(settingsContext.pages);

		return {
			...settingsContext,
			pages: visitor.mapFields((field) => {
				const updatedField = {
					...field,
					defaultLanguageId,
					editingLanguageId,
					keywordReadOnly:
						field.fieldName == 'columns' ||
						field.fieldName == 'options' ||
						field.fieldName == 'rows'
							? readOnlyFieldName
							: false,
					readOnly:
						field.fieldName == 'name' ? readOnlyFieldName : false,
				};

				return {
					...updatedField,
					name: generateName(field.name, updatedField),
				};
			}),
		};
	}

	open() {
		const {container} = this.refs;
		const {open} = this.state;
		const {transitionEnd} = this;

		if (open) {
			return;
		}

		dom.once(container, transitionEnd, () => {
			if (this._isEditMode()) {
				const firstInput = this.element.querySelector('input');

				if (firstInput && !container.contains(document.activeElement)) {
					firstInput.focus();
					selectText(firstInput);
				}
			}
		});

		this.setState({
			activeTab: 0,
			open: true,
		});

		this.refreshDragAndDrop();
	}

	refreshDragAndDrop() {
		this._dragAndDrop.setState({
			targets: this.getDropTargetsSelector(),
		});
	}

	render() {
		const {activeTab, open} = this.state;
		const {spritemap} = this.props;
		const editMode = this._isEditMode();
		const styles = classnames('sidebar-container', {open});

		return (
			<div>
				<div class={styles} ref="container">
					<div class="sidebar sidebar-light">
						<nav class="component-tbar tbar">
							<div class="container-fluid">
								{this._renderTopBar()}
							</div>
						</nav>
						<nav class="component-navigation-bar navbar navigation-bar navbar-collapse-absolute navbar-expand-md navbar-underline">
							<a
								aria-controls="sidebarLightCollapse00"
								aria-expanded="false"
								aria-label="Toggle Navigation"
								class="collapsed navbar-toggler navbar-toggler-link"
								data-toggle="liferay-collapse"
								href="#sidebarLightCollapse00"
								role="button"
							>
								<span class="navbar-text-truncate">
									{'Details'}
								</span>
								<svg
									aria-hidden="true"
									class="lexicon-icon lexicon-icon-caret-bottom"
								>
									<use
										xlink:href={`${spritemap}#caret-bottom`}
									/>
								</svg>
							</a>
							<div
								class="collapse navbar-collapse"
								id="sidebarLightCollapse00"
							>
								<ul class="nav navbar-nav" role="tablist">
									{this._renderNavItems()}
								</ul>
							</div>
						</nav>
						<div class="ddm-sidebar-body">
							{!editMode &&
								activeTab == 0 &&
								this._renderFieldTypeGroups()}

							{!editMode &&
								activeTab == 1 &&
								this._renderElementSets()}

							{editMode && (
								<div class="sidebar-body ddm-field-settings">
									<div class="tab-content">
										<form>
											{this._renderSettingsForm()}
										</form>
									</div>
								</div>
							)}
						</div>
					</div>
				</div>

				<ClayModal
					body={Liferay.Language.get(
						'are-you-sure-you-want-to-cancel'
					)}
					events={{
						clickButton: this._handleCancelChangesModalButtonClicked.bind(
							this
						),
					}}
					footerButtons={[
						{
							alignment: 'right',
							label: Liferay.Language.get('dismiss'),
							style: 'primary',
							type: 'close',
						},
						{
							alignment: 'right',
							label: Liferay.Language.get('yes-cancel'),
							style: 'primary',
							type: 'button',
						},
					]}
					ref="cancelChangesModal"
					size="sm"
					spritemap={spritemap}
					title={Liferay.Language.get(
						'cancel-field-changes-question'
					)}
				/>
			</div>
		);
	}

	syncEditingLanguageId() {
		const {dispatch} = this.context;
		const {evaluableForm} = this.refs;
		const {editingLanguageId, focusedField} = this.props;

		if (evaluableForm && evaluableForm.reactComponentRef.current) {
			evaluableForm.reactComponentRef.current
				.evaluate(editingLanguageId)
				.then((pages) => {
					dispatch('focusedFieldEvaluationEnded', {
						...focusedField,
						changedEditingLanguage: true,
						settingsContext: {
							...focusedField.settingsContext,
							pages,
						},
					});
				})
				.catch((error) => dispatch('evaluationError', error));
		}
	}

	syncVisible(visible) {
		if (!visible) {
			this.dispatchFieldBlurred();
		}
	}

	_bindDragAndDrop() {
		this._dragAndDrop = new DragDrop({
			container: document.body,
			dragPlaceholder: Drag.Placeholder.CLONE,
			sources: '.ddm-drag-item',
			targets: this.getDropTargetsSelector(),
			useShim: false,
		});

		this._eventHandler.add(
			this._dragAndDrop.on(Drag.Events.START, this._handleDragStarted),
			this._dragAndDrop.on(DragDrop.Events.END, this._handleDragEnded),
			this._dragAndDrop.on(
				DragDrop.Events.TARGET_ENTER,
				this._handleDragTargetEnter
			),
			this._dragAndDrop.on(
				DragDrop.Events.TARGET_LEAVE,
				this._handleDragTargetLeave
			)
		);
	}

	_cancelFieldChanges() {
		const {cancelChangesModal} = this.refs;

		cancelChangesModal.show();
	}

	_deleteField(fieldName) {
		const {dispatch} = this.context;

		dispatch('fieldDeleted', {fieldName});
	}

	dispatchFieldBlurred() {
		const {dispatch} = this.context;

		if (!this.isDisposed()) {
			dispatch('sidebarFieldBlurred');
		}
	}

	_dropdownFieldTypesValueFn() {
		const {fieldTypes} = this.props;

		return fieldTypes
			.filter(({system}) => {
				return !system;
			})
			.map((fieldType) => {
				return {
					...fieldType,
					type: 'item',
				};
			});
	}

	_duplicateField(fieldName) {
		const {dispatch} = this.context;

		dispatch('fieldDuplicated', {fieldName});
	}

	_fetchElementSet(fieldSetId) {
		const {
			editingLanguageId,
			fieldSetDefinitionURL,
			groupId,
			portletNamespace,
		} = this.props;

		return makeFetch({
			method: 'GET',
			url: `${fieldSetDefinitionURL}?ddmStructureId=${fieldSetId}&languageId=${editingLanguageId}&portletNamespace=${portletNamespace}&scopeGroupId=${groupId}`,
		})
			.then(({pages}) => pages)
			.catch((error) => {
				throw new Error(error);
			});
	}

	_fieldTypesGroupValueFn() {
		const {fieldTypes} = this.props;
		const group = {
			basic: {
				fields: [],
				label: Liferay.Language.get('field-types-basic-elements'),
			},
			customized: {
				fields: [],
				label: Liferay.Language.get('field-types-customized-elements'),
			},
		};

		return fieldTypes.reduce((prev, next) => {
			if (next.group && !next.system) {
				if (next.group === 'interface') {
					prev.basic.fields.push(next);
				}
				else {
					prev[next.group].fields.push(next);
				}
			}

			return prev;
		}, group);
	}

	_getTabItems() {
		if (!this._isEditMode()) {
			return this.state.tabs;
		}

		const {focusedField} = this.props;
		const {settingsContext} = focusedField;

		return settingsContext.pages.map(({title}) => title);
	}

	_getTransitionEndEvent() {
		const el = document.createElement('metalClayTransitionEnd');

		const transitionEndEvents = {
			MozTransition: 'transitionend',
			OTransition: 'oTransitionEnd otransitionend',
			WebkitTransition: 'webkitTransitionEnd',
			transition: 'transitionend',
		};

		let eventName = false;

		Object.keys(transitionEndEvents).some((name) => {
			if (el.style[name] !== undefined) {
				eventName = transitionEndEvents[name];

				return true;
			}
		});

		return eventName;
	}

	_handleCancelChangesModalButtonClicked(event) {
		const {dispatch} = this.context;
		const {target} = event;
		const {cancelChangesModal} = this.refs;

		event.stopPropagation();

		if (this._isOutsideModal(target)) {
			this.close();
		}

		cancelChangesModal.emit('hide');

		if (!event.target.classList.contains('close-modal')) {
			dispatch('fieldChangesCanceled', {});
		}
	}

	_handleChangeFieldTypeItemClicked({data}) {
		const newFieldType = data.item.name;

		this.changeFieldType(newFieldType);
	}

	_handleCloseButtonClicked() {
		this.close();
	}

	_handleDocumentMouseDown({target}) {
		const {transitionEnd} = this;
		const {open} = this.state;

		if (
			this._isCloseButton(target) ||
			(open &&
				!this._isControlProductMenuItem(target) &&
				!this._isProductMenuSidebarItem(target) &&
				!this._isSidebarElement(target) &&
				!this._isTranslationItem(target))
		) {
			this.close();

			dom.once(this.refs.container, transitionEnd, () =>
				this.dispatchFieldBlurred()
			);

			if (!this._isModalElement(target)) {
				setTimeout(() => this.dispatchFieldBlurred(), 500);
			}
		}
	}

	_handleDragEnded(data, event) {
		const {dispatch} = this.context;

		event.preventDefault();

		if (!data.target) {
			return;
		}

		this._handleDragTargetLeave(data);

		const {fieldTypes} = this.props;
		const {fieldSetId} = data.source.dataset;
		const columnNode = dom.closest(data.target, '.col-ddm');
		const indexes = FormSupport.getIndexes(columnNode);

		if (fieldSetId) {
			this._fetchElementSet(fieldSetId).then((pages) => {
				dispatch('elementSetAdded', {
					data,
					fieldSetId,
					fieldSetPages: pages,
					indexes,
				});
			});
		}
		else {
			const fieldType = fieldTypes.find(({name}) => {
				return name === data.source.dataset.fieldTypeName;
			});
			let parentFieldName;
			const parentFieldNode = dom.closest(
				data.target.parentElement,
				'.ddm-field'
			);

			if (parentFieldNode) {
				parentFieldName = parentFieldNode.dataset.fieldName;
			}

			const payload = {
				data: {
					...data,
					fieldName: data.target.dataset.fieldName,
					parentFieldName,
				},
				fieldType: {
					...fieldType,
					editable: true,
				},
				indexes,
			};

			if (dom.closest(data.target, '.col-empty')) {
				const addedToPlaceholder = dom.closest(
					data.target,
					'.placeholder'
				);

				dispatch('fieldAdded', {
					...payload,
					addedToPlaceholder,
				});
			}
			else {
				dispatch('sectionAdded', payload);
			}
		}
	}

	_handleDragStarted() {
		this.refreshDragAndDrop();

		this.close();
	}

	_handleDragTargetEnter({target}) {
		const parentFieldNode = dom.closest(
			target.parentElement,
			`.ddm-field-container`
		);

		if (parentFieldNode) {
			parentFieldNode.classList.add('active-drop-child');
		}
	}

	_handleDragTargetLeave({target}) {
		const parentFieldNode = dom.closest(
			target.parentElement,
			`.ddm-field-container`
		);

		if (parentFieldNode) {
			parentFieldNode.classList.remove('active-drop-child');
		}
	}

	_handleEvaluatorChanged(pages) {
		const {dispatch} = this.context;
		const {focusedField} = this.props;

		dispatch('focusedFieldEvaluationEnded', {
			...focusedField,
			settingsContext: {
				...focusedField.settingsContext,
				pages,
			},
		});
	}

	_handleElementSettingsClicked({data: {item}}) {
		const {
			columnIndex,
			fieldName,
			pageIndex,
			rowIndex,
		} = this.props.focusedField;
		const {settingsItem} = item;
		const indexes = {
			columnIndex,
			pageIndex,
			rowIndex,
		};

		if (!item.disabled) {
			if (settingsItem === 'duplicate-field') {
				this._duplicateField(fieldName);
			}
			else if (settingsItem === 'delete-field') {
				this._deleteField(fieldName);
			}
			else if (settingsItem === 'cancel-field-changes') {
				this._cancelFieldChanges(indexes);
			}
		}
	}

	_handlePreviousButtonClicked() {
		const {transitionEnd} = this;

		this.close();

		dom.once(this.refs.container, transitionEnd, () => {
			this.dispatchFieldBlurred();
			this.open();
		});
	}

	_handleSettingsFieldBlurred({fieldInstance, value}) {
		const {dispatch} = this.context;
		const {editingLanguageId} = this.props;
		const {fieldName} = fieldInstance;

		dispatch('fieldBlurred', {
			editingLanguageId,
			propertyName: fieldName,
			propertyValue: value,
		});
	}

	_handleSettingsFieldEdited({fieldInstance, value}) {
		if (fieldInstance && !fieldInstance.isDisposed() && this.state.open) {
			const {editingLanguageId} = this.props;
			const {fieldName} = fieldInstance;
			const {dispatch} = this.context;

			dispatch('fieldEdited', {
				editingLanguageId,
				propertyName: fieldName,
				propertyValue: value,
			});
		}
	}

	_handleSettingsFormAttached() {
		const reactForm = this.refs.evaluableForm.reactComponentRef.current;
		const {editingLanguageId} = this.props;

		if (reactForm) {
			reactForm.evaluate(editingLanguageId);
		}
	}

	_handleTabItemClicked(event) {
		const {target} = event;
		const {
			dataset: {index},
		} = dom.closest(target, '.nav-item');

		event.preventDefault();

		this.setState({
			activeTab: parseInt(index, 10),
		});
	}

	_isCloseButton(node) {
		const {closeButton} = this.refs;

		return closeButton.contains(node);
	}

	_isControlProductMenuItem(node) {
		return !!dom.closest(node, '.sidenav-toggler');
	}

	_isEditMode() {
		const {focusedField} = this.props;

		return Object.keys(focusedField).length > 0;
	}

	_isModalElement(node) {
		return dom.closest(node, '.modal');
	}

	_isProductMenuSidebarItem(node) {
		return !!dom.closest(node, '.sidenav-menu');
	}

	_isOutsideModal(node) {
		return !dom.closest(node, '.close-modal');
	}

	_isSettingsElement(target) {
		const {fieldSettingsActions} = this.refs;
		let dropdownPortal;

		if (fieldSettingsActions) {
			const {dropdown} = fieldSettingsActions.refs;
			const {portal} = dropdown.refs;

			dropdownPortal = portal.element.contains(target);
		}

		return dropdownPortal;
	}

	_isSidebarElement(node) {
		const {element} = this;
		const alloyEditorToolbarNode = dom.closest(node, '.ae-ui');
		const fieldColumnNode = dom.closest(node, '.col-ddm');
		const fieldTypesDropdownNode = dom.closest(node, '.dropdown-menu');

		return (
			alloyEditorToolbarNode ||
			fieldTypesDropdownNode ||
			fieldColumnNode ||
			element.contains(node) ||
			this._isSettingsElement(node)
		);
	}

	_isTranslationItem(node) {
		return !!dom.closest(node, '.lfr-translationmanager');
	}

	_mergeFieldTypeSettings(oldSettingsContext, newSettingsContext) {
		const newVisitor = new PagesVisitor(newSettingsContext.pages);
		const oldVisitor = new PagesVisitor(oldSettingsContext.pages);

		const excludedFields = ['indexType', 'readOnly', 'type', 'validation'];

		const getPreviousField = ({fieldName, type}) => {
			let field;

			oldVisitor.findField((oldField) => {
				if (
					excludedFields.indexOf(fieldName) === -1 &&
					oldField.fieldName === fieldName &&
					oldField.type === type
				) {
					field = oldField;
				}

				return field;
			});

			return field;
		};

		return {
			...newSettingsContext,
			pages: newVisitor.mapFields((newField) => {
				if (newField.visible) {
					const previousField = getPreviousField(newField);

					if (previousField) {
						newField.value = previousField.value;

						if (newField.localizable && previousField.localizable) {
							newField.localizedValue = {
								...previousField.localizedValue,
							};
						}
					}

					if (newField.fieldName == 'predefinedValue') {
						delete newField.value;

						newField.localizedValue = {};

						if (newField.options) {
							newField.options = this._getPredefinedOptions(
								newVisitor
							);
						}
					}
				}

				return newField;
			}),
		};
	}

	_getPredefinedOptions(visitor) {
		const options = visitor.findField((field) => {
			return field.fieldName == 'options';
		});

		if (options) {
			const locale = options.locale;

			return options.value[locale];
		}

		return options;
	}

	_renderElementSets() {
		const {fieldSets} = this.props;
		const groups = Object.keys(fieldSets);

		let elementSetsArea = '';

		if (groups.length > 0) {
			elementSetsArea = this._renderElementSetsGroups(groups);
		}
		else {
			elementSetsArea = this._renderEmptyElementSets();
		}

		return elementSetsArea;
	}

	_renderElementSetsGroups(groups) {
		const {fieldSets, spritemap} = this.props;

		return (
			<div
				aria-orientation="vertical"
				class="ddm-field-types-panel panel-group"
				id="accordion03"
				role="tablist"
			>
				{groups.map((key) => (
					<div
						aria-labelledby={`#ddm-field-types-${key}-header`}
						class="panel-collapse show"
						id={`ddm-field-types-${key}-body`}
						key={key}
						role="tabpanel"
					>
						<div class="panel-body p-0 m-0 list-group">
							<div
								class="ddm-drag-item list-group-item list-group-item-flex"
								data-field-set-id={fieldSets[key].id}
								data-field-set-name={fieldSets[key].name}
								key={`fieldType_${fieldSets[key].name}`}
								ref={`fieldType_${fieldSets[key].name}`}
							>
								<div class="autofit-col">
									<span class="sticker sticker-secondary">
										<span class="inline-item">
											<svg
												aria-hidden="true"
												class={`lexicon-icon lexicon-icon-${fieldSets[key].icon}`}
											>
												<use
													xlink:href={`${spritemap}#${fieldSets[key].icon}`}
												/>
											</svg>
										</span>
									</span>
								</div>
								<div class="autofit-col autofit-col-expand">
									<h4 class="list-group-title text-truncate">
										<span>{fieldSets[key].name}</span>
									</h4>
								</div>
							</div>
						</div>
					</div>
				))}
			</div>
		);
	}

	_renderEmptyElementSets() {
		return (
			<div class="list-group-body  list-group">
				<div class="main-content-body">
					<div class="text-center text-muted">
						<p class="text-default">
							{Liferay.Language.get(
								'there-are-no-element-sets-yet'
							)}
						</p>
					</div>
				</div>
			</div>
		);
	}

	_renderFieldTypeDropdownLabel() {
		const {fieldTypes, focusedField, spritemap} = this.props;
		const {icon, label} = fieldTypes.find(
			({name}) => name === focusedField.type
		);

		return (
			<Fragment>
				<ClayIcon
					elementClasses={'inline-item inline-item-before'}
					spritemap={spritemap}
					symbol={icon}
				/>
				{label}
				<ClayIcon
					elementClasses={'inline-item inline-item-after'}
					spritemap={spritemap}
					symbol={'caret-bottom'}
				/>
			</Fragment>
		);
	}

	_renderFieldTypeGroups() {
		const {spritemap} = this.props;
		const {fieldTypesGroup} = this.state;
		const group = Object.keys(fieldTypesGroup);

		return (
			<div
				aria-orientation="vertical"
				class="ddm-field-types-panel panel-group"
				id="accordion03"
				role="tablist"
			>
				{group.map((key, index) => (
					<div
						class="panel panel-secondary"
						key={`fields-group-${key}-${index}`}
					>
						<a
							aria-controls="collapseTwo"
							aria-expanded="true"
							class="collapse-icon panel-header panel-header-link"
							data-parent="#accordion03"
							data-toggle="liferay-collapse"
							href={`#ddm-field-types-${key}-body`}
							id={`ddm-field-types-${key}-header`}
							role="tab"
						>
							<span class="panel-title">
								{fieldTypesGroup[key].label}
							</span>
							<span class="collapse-icon-closed">
								<svg
									aria-hidden="true"
									class="lexicon-icon lexicon-icon-angle-right"
								>
									<use
										xlink:href={`${spritemap}#angle-right`}
									/>
								</svg>
							</span>
							<span class="collapse-icon-open">
								<svg
									aria-hidden="true"
									class="lexicon-icon lexicon-icon-angle-down"
								>
									<use
										xlink:href={`${spritemap}#angle-down`}
									/>
								</svg>
							</span>
						</a>
						<div
							aria-labelledby={`#ddm-field-types-${key}-header`}
							class="panel-collapse show"
							id={`ddm-field-types-${key}-body`}
							role="tabpanel"
						>
							<div class="panel-body p-0 m-0 list-group">
								{fieldTypesGroup[key].fields.map(
									(fieldType) => (
										<FieldTypeBox
											fieldType={fieldType}
											key={fieldType.name}
											spritemap={spritemap}
										/>
									)
								)}
							</div>
						</div>
					</div>
				))}
			</div>
		);
	}

	_renderNavItems() {
		const {activeTab} = this.state;

		return this._getTabItems().map((name, index) => {
			const style = classnames('nav-link', {
				active: index === activeTab,
			});

			return (
				<li
					class="nav-item"
					data-index={index}
					data-onclick={this._handleTabItemClicked}
					key={`tab${index}`}
					ref={`tab${index}`}
				>
					<a
						aria-controls="sidebarLightDetails"
						class={style}
						data-toggle="liferay-tab"
						href="javascript:;"
						role="tab"
					>
						<span class="navbar-text-truncate">{name}</span>
					</a>
				</li>
			);
		});
	}

	_renderSettingsForm() {
		const {activeTab} = this.state;
		const {
			defaultLanguageId,
			editingLanguageId,
			portletNamespace,
			spritemap,
		} = this.props;
		const {pages, rules} = this.getSettingsFormContext();
		const sidebarTabIndex = pages.length - 1;

		if (sidebarTabIndex < activeTab) {
			this.setState({
				activeTab: sidebarTabIndex,
			});
		}

		return (
			<Form
				activePage={activeTab}
				defaultLanguageId={defaultLanguageId}
				editable={true}
				editingLanguageId={editingLanguageId}
				events={{
					attached: this._handleSettingsFormAttached,
					evaluated: this._handleEvaluatorChanged,
					fieldBlurred: this._handleSettingsFieldBlurred,
					fieldEdited: this._handleSettingsFieldEdited,
				}}
				pages={pages}
				paginationMode="tabbed"
				portletNamespace={portletNamespace}
				ref={`evaluableForm`}
				rules={rules}
				spritemap={spritemap}
			/>
		);
	}

	_renderTopBar() {
		const {fieldTypes, focusedField, spritemap} = this.props;
		const editMode = this._isEditMode();
		const fieldActions = [
			{
				label: Liferay.Language.get('duplicate-field'),
				settingsItem: 'duplicate-field',
			},
			{
				label: Liferay.Language.get('remove-field'),
				settingsItem: 'delete-field',
			},
			{
				label: Liferay.Language.get('cancel-field-changes'),
				settingsItem: 'cancel-field-changes',
			},
		];
		const focusedFieldType = fieldTypes.find(
			({name}) => name === focusedField.type
		);
		const previousButtonEvents = {
			click: this._handlePreviousButtonClicked,
		};

		return (
			<ul class="tbar-nav">
				{!editMode && (
					<li class="tbar-item tbar-item-expand text-left">
						<div class="tbar-section">
							<span class="text-truncate-inline">
								<span class="text-truncate">
									{Liferay.Language.get('add-elements')}
								</span>
							</span>
						</div>
					</li>
				)}
				{editMode && (
					<Fragment>
						<li class="tbar-item">
							<ClayButton
								elementClasses="nav-link"
								events={previousButtonEvents}
								icon="angle-left"
								ref="previousButton"
								size="sm"
								spritemap={spritemap}
								style="secondary"
							/>
						</li>
						<li class="tbar-item ddm-fieldtypes-dropdown tbar-item-expand text-left">
							<div>
								<ClayDropdownBase
									events={{
										itemClicked: this
											._handleChangeFieldTypeItemClicked,
									}}
									icon={focusedFieldType.icon}
									items={this.state.dropdownFieldTypes}
									itemsIconAlignment={'left'}
									label={this._renderFieldTypeDropdownLabel}
									spritemap={spritemap}
									style={'secondary'}
									triggerClasses={'nav-link btn-sm'}
								/>
							</div>
						</li>
						<li class="tbar-item">
							<ClayActionsDropdown
								events={{
									itemClicked: this
										._handleElementSettingsClicked,
								}}
								items={fieldActions}
								ref="fieldSettingsActions"
								spritemap={spritemap}
								triggerClasses={'component-action'}
							/>
						</li>
					</Fragment>
				)}
				<li class="tbar-item">
					<a
						class="component-action sidebar-close"
						data-onclick={this._handleCloseButtonClicked}
						href="javascript:;"
						ref="closeButton"
						role="button"
					>
						<svg
							aria-hidden="true"
							class="lexicon-icon lexicon-icon-times"
						>
							<use xlink:href={`${spritemap}#times`} />
						</svg>
					</a>
				</li>
			</ul>
		);
	}
}

Sidebar.STATE = {

	/**
	 * @default 0
	 * @instance
	 * @memberof Sidebar
	 * @type {?number}
	 */

	activeTab: Config.number().value(0).internal(),

	/**
	 * @default _dropdownFieldTypesValueFn
	 * @instance
	 * @memberof Sidebar
	 * @type {?array}
	 */

	dropdownFieldTypes: Config.array().valueFn('_dropdownFieldTypesValueFn'),

	/**
	 * @instance
	 * @memberof Sidebar
	 * @type {array}
	 */

	fieldTypesGroup: Config.object().valueFn('_fieldTypesGroupValueFn'),

	/**
	 * @default false
	 * @instance
	 * @memberof Sidebar
	 * @type {?bool}
	 */

	open: Config.bool().internal().value(false),

	/**
	 * @default object
	 * @instance
	 * @memberof Sidebar
	 * @type {?object}
	 */

	tabs: Config.object()
		.value([
			Liferay.Language.get('elements'),
			Liferay.Language.get('element-sets'),
		])
		.internal(),
};

Sidebar.PROPS = {

	/**
	 * @default undefined
	 * @instance
	 * @memberof Sidebar
	 * @type {?string}
	 */

	defaultLanguageId: Config.string(),

	/**
	 * @default undefined
	 * @instance
	 * @memberof Sidebar
	 * @type {?string}
	 */

	editingLanguageId: Config.string(),

	/**
	 * @default undefined
	 * @instance
	 * @memberof Sidebar
	 * @type {?string}
	 */

	fieldSetDefinitionURL: Config.string(),

	/**
	 * @default []
	 * @instance
	 * @memberof Sidebar
	 * @type {?(array|undefined)}
	 */

	fieldSets: Config.array().value([]),

	/**
	 * @default []
	 * @instance
	 * @memberof Sidebar
	 * @type {?(array|undefined)}
	 */

	fieldTypes: Config.array().value([]),

	/**
	 * @default {}
	 * @instance
	 * @memberof Sidebar
	 * @type {?object}
	 */

	focusedField: focusedFieldStructure.value({}),

	/**
	 * @default undefined
	 * @instance
	 * @memberof Sidebar
	 * @type {?string}
	 */

	portletNamespace: Config.string(),

	/**
	 * @default undefined
	 * @instance
	 * @memberof Sidebar
	 * @type {?bool}
	 */

	readOnlyFieldName: Config.bool().value(true),

	/**
	 * @default undefined
	 * @instance
	 * @memberof Sidebar
	 * @type {?(string|undefined)}
	 */

	spritemap: Config.string().required(),
};

export default Sidebar;