import * as FormSupport from '../Form/FormSupport.es';
import classnames from 'classnames';
import ClayButton from 'clay-button';
import Component, {Fragment} from 'metal-jsx';
import dom from 'metal-dom';
import FieldTypeBox from '../FieldTypeBox/FieldTypeBox.es.js';
import FormRenderer from '../Form/FormRenderer.es';
import UA from 'metal-useragent';
import WithEvaluator from '../Form/Evaluator.es';
import {ClayActionsDropdown, ClayDropdownBase} from 'clay-dropdown';
import {ClayIcon} from 'clay-icon';
import {Config} from 'metal-state';
import {Drag, DragDrop} from 'metal-drag-drop';
import {EventHandler} from 'metal-events';
import {focusedFieldStructure} from '../../util/config.es';
import {getFieldProperties, normalizeSettingsContextPages} from '../../util/fieldSupport.es';
import {PagesVisitor, RulesVisitor} from '../../util/visitors.es';
import {selectText} from '../../util/dom.es';
const EVALUATOR_URL = '/o/dynamic-data-mapping-form-context-provider/';
const FormWithEvaluator = WithEvaluator(FormRenderer);
/**
* Sidebar is a tooling to mount forms.
*/
class Sidebar extends Component {
static 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(
{
add: {
items: [
Liferay.Language.get('elements'),
Liferay.Language.get('element-sets')
]
},
edit: {
items: [
Liferay.Language.get('basic'),
Liferay.Language.get('properties')
]
}
}
).internal()
};
static PROPS = {
/**
* @default undefined
* @instance
* @memberof Sidebar
* @type {?string}
*/
defaultLanguageId: Config.string(),
/**
* @default undefined
* @instance
* @memberof Sidebar
* @type {?string}
*/
editingLanguageId: Config.string(),
/**
* @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|undefined)}
*/
spritemap: Config.string().required()
};
attached() {
this._bindDragAndDrop();
this._eventHandler.add(
dom.on(document, 'mousedown', this._handleDocumentMouseDown, true)
);
}
changeFieldType(type) {
const {defaultLanguageId, editingLanguageId, fieldTypes, focusedField} = this.props;
const newFieldType = fieldTypes.find(({name}) => name === type);
const newSettingsContext = {
...newFieldType.settingsContext,
pages: normalizeSettingsContextPages(newFieldType.settingsContext.pages, editingLanguageId, newFieldType, focusedField.fieldName)
};
let {settingsContext} = focusedField;
if (type !== focusedField.type) {
settingsContext = this._mergeFieldTypeSettings(settingsContext, newSettingsContext);
}
this.emit(
'focusedFieldUpdated',
{
...focusedField,
...newFieldType,
...getFieldProperties(settingsContext, defaultLanguageId, editingLanguageId),
settingsContext,
type: newFieldType.name
}
);
this.refs.evaluableForm.evaluate();
}
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._handleEvaluatorChanged = this._handleEvaluatorChanged.bind(this);
this._handleFieldSettingsClicked = this._handleFieldSettingsClicked.bind(this);
this._handlePreviousButtonClicked = this._handlePreviousButtonClicked.bind(this);
this._handleSettingsFieldBlurred = this._handleSettingsFieldBlurred.bind(this);
this._handleSettingsFieldEdited = this._handleSettingsFieldEdited.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();
this.emit('fieldBlurred');
}
getFormContext() {
const {defaultLanguageId, editingLanguageId, focusedField} = this.props;
const {settingsContext} = focusedField;
const visitor = new PagesVisitor(settingsContext.pages);
return {
...settingsContext,
pages: visitor.mapFields(
field => {
return {
...field,
defaultLanguageId,
editingLanguageId,
readOnly: this.isFieldReadOnly(field)
};
}
)
};
}
isActionsDisabled() {
const {defaultLanguageId, editingLanguageId} = this.props;
return defaultLanguageId !== editingLanguageId;
}
isChangeFieldTypeEnabled() {
return !this.isActionsDisabled();
}
isFieldReadOnly({localizable, type}) {
const {defaultLanguageId, editingLanguageId} = this.props;
return (
defaultLanguageId !== editingLanguageId && (
!localizable ||
type === 'validation'
)
);
}
open() {
const {transitionEnd} = this;
dom.once(
this.refs.container,
transitionEnd,
() => {
if (this._isEditMode()) {
const firstInput = this.element.querySelector('input');
if (firstInput && document.activeElement !== firstInput) {
firstInput.focus();
selectText(firstInput);
}
}
}
);
this.setState(
{
activeTab: 0,
open: true
}
);
this.refreshDragAndDrop();
}
refreshDragAndDrop() {
this._dragAndDrop.setState(
{
targets: UA.isIE ? this._dragAndDrop.setterTargetsFn_('.ddm-target') : '.ddm-target'
}
);
}
render() {
const {activeTab, open} = this.state;
const {
editingLanguageId,
focusedField,
spritemap
} = this.props;
const layoutRenderEvents = {
evaluated: this._handleEvaluatorChanged,
fieldBlurred: this._handleSettingsFieldBlurred,
fieldEdited: this._handleSettingsFieldEdited
};
const editMode = this._isEditMode();
const styles = classnames('sidebar-container', {open});
return (
<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="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">
<FormWithEvaluator
activePage={activeTab}
editable={true}
editingLanguageId={editingLanguageId}
events={layoutRenderEvents}
fieldType={focusedField.type}
formContext={this.getFormContext()}
paginationMode="tabbed"
ref="evaluableForm"
spritemap={spritemap}
url={EVALUATOR_URL}
/>
</div>
</div>
)}
</div>
</div>
</div>
);
}
syncEditingLanguageId() {
const {evaluableForm} = this.refs;
if (evaluableForm) {
evaluableForm.evaluate();
}
}
syncVisible(visible) {
if (!visible) {
this.emit('fieldBlurred');
}
}
_bindDragAndDrop() {
this._dragAndDrop = new DragDrop(
{
dragPlaceholder: Drag.Placeholder.CLONE,
sources: '.ddm-drag-item',
targets: '.ddm-target',
useShim: false
}
);
this._eventHandler.add(
this._dragAndDrop.on(
DragDrop.Events.END,
this._handleDragEnded
),
this._dragAndDrop.on(Drag.Events.START, this._handleDragStarted)
);
}
_cancelFieldChanges(indexes) {
this.emit('fieldChangesCanceled', indexes);
}
_deleteField(indexes) {
this.emit('fieldDeleted', {indexes});
}
_dropdownFieldTypesValueFn() {
const {fieldTypes} = this.props;
return fieldTypes.filter(
({system}) => {
return !system;
}
).map(
fieldType => {
return {
...fieldType,
type: 'item'
};
}
);
}
_duplicateField(indexes) {
this.emit('fieldDuplicated', {indexes});
}
_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) {
prev[next.group].fields.push(next);
}
return prev;
},
group
);
}
_getTransitionEndEvent() {
const el = document.createElement('metalClayTransitionEnd');
const transitionEndEvents = {
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend',
WebkitTransition: 'webkitTransitionEnd'
};
let eventName = false;
for (const name in transitionEndEvents) {
if (el.style[name] !== undefined) {
eventName = transitionEndEvents[name];
break;
}
}
return eventName;
}
_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._isSidebarElement(target) &&
!this._isTranslationItem(target)
))
) {
this.close();
dom.once(
this.refs.container,
transitionEnd,
() => this.emit('fieldBlurred')
);
if (!this._isModalElement(target)) {
setTimeout(() => this.emit('fieldBlurred'), 500);
}
}
}
_handleDragEnded(data, event) {
event.preventDefault();
if (!data.target) {
return;
}
const {fieldTypes} = this.props;
const {fieldSetId} = data.source.dataset;
const target = FormSupport.getIndexes(data.target.parentElement);
if (fieldSetId) {
this.emit(
'fieldSetAdded',
{
data,
fieldSetId,
target
}
);
}
else {
const fieldType = fieldTypes.find(
({name}) => {
return name === data.source.dataset.fieldTypeName;
}
);
this.emit(
'fieldAdded',
{
data,
fieldType: {
...fieldType,
editable: true
},
target
}
);
}
}
_handleDragStarted() {
this.refreshDragAndDrop();
this.close();
}
_handleEvaluatorChanged(pages) {
const {focusedField} = this.props;
this.emit(
'focusedFieldUpdated',
{
...focusedField,
settingsContext: {
...focusedField.settingsContext,
pages
}
}
);
}
_handleFieldSettingsClicked({data: {item}}) {
const {columnIndex, pageIndex, rowIndex} = this.props.focusedField;
const {settingsItem} = item;
const indexes = {
columnIndex,
pageIndex,
rowIndex
};
if (!item.disabled) {
if (settingsItem === 'duplicate-field') {
this._duplicateField(indexes);
}
else if (settingsItem === 'delete-field') {
this._deleteField(indexes);
}
else if (settingsItem === 'cancel-field-changes') {
this._cancelFieldChanges(indexes);
}
}
}
_handlePreviousButtonClicked() {
const {transitionEnd} = this;
this.close();
dom.once(
this.refs.container,
transitionEnd,
() => {
this.emit('fieldBlurred');
this.open();
}
);
}
_handleSettingsFieldBlurred(event) {
this.emit('settingsFieldBlurred', event);
}
_handleSettingsFieldEdited(event) {
this.emit('settingsFieldEdited', event);
}
_handleTabItemClicked(event) {
const {target} = event;
const {dataset: {index}} = dom.closest(target, '.nav-item');
event.preventDefault();
this.setState(
{
activeTab: parseInt(index, 10)
}
);
}
_hasRuleExpression(fieldName) {
const {rules} = this.props;
const visitor = new RulesVisitor(rules);
return visitor.containsFieldExpression(fieldName);
}
_isCloseButton(node) {
const {closeButton} = this.refs;
return closeButton.contains(node);
}
_isEditMode() {
const {focusedField} = this.props;
return !(
Object.keys(focusedField).length === 0 &&
focusedField.constructor === Object
);
}
_isModalElement(node) {
return dom.closest(node, '.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',
'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 => {
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;
}
}
return newField;
}
)
};
}
_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="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, tabs} = this.state;
return tabs[this._isEditMode() ? 'edit' : 'add'].items.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="tab"
href="javascript:;"
role="tab"
>
<span class="navbar-text-truncate">{name}</span>
</a>
</li>
);
}
);
}
_renderTopBar() {
const {fieldTypes, focusedField, spritemap} = this.props;
const editMode = this._isEditMode();
const fieldActions = [
{
disabled: this.isActionsDisabled(),
label: Liferay.Language.get('duplicate-field'),
settingsItem: 'duplicate-field'
},
{
disabled: this.isActionsDisabled(),
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
disabled={this.isActionsDisabled()}
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
disabled={!this.isChangeFieldTypeEnabled()}
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._handleFieldSettingsClicked
}}
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="#1"
ref="closeButton"
role="button"
>
<svg
aria-hidden="true"
class="lexicon-icon lexicon-icon-times"
>
<use
xlink:href={`${spritemap}#times`}
/>
</svg>
</a>
</li>
</ul>
);
}
}
export default Sidebar;