src/components/chat-item/chat-item-form-items.ts (328 lines of code) (raw):
/*!
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { Config } from '../../helper/config';
import { DomBuilder, ExtendedHTMLElement } from '../../helper/dom';
import { cancelEvent } from '../../helper/events';
import testIds from '../../helper/test-ids';
import { isMandatoryItemValid, isTextualFormItemValid } from '../../helper/validator';
import { ChatItem, ChatItemFormItem, TextBasedFormItem } from '../../static';
import { Card } from '../card/card';
import { CardBody } from '../card/card-body';
import { Checkbox } from '../form-items/checkbox';
import { RadioGroup } from '../form-items/radio-group';
import { Select } from '../form-items/select';
import { Stars } from '../form-items/stars';
import { Switch } from '../form-items/switch';
import { TextArea } from '../form-items/text-area';
import { TextInput } from '../form-items/text-input';
import { Icon, MynahIcons } from '../icon';
import { Overlay, OverlayHorizontalDirection, OverlayVerticalDirection } from '../overlay';
const TOOLTIP_DELAY = 350;
export interface ChatItemFormItemsWrapperProps {
tabId: string;
chatItem: Partial<ChatItem>;
classNames?: string[];
onModifierEnterPress?: (formData: Record<string, any>, tabId: string) => void;
onTextualItemKeyPress?: (event: KeyboardEvent, itemId: string, formData: Record<string, any>, tabId: string, disableAllCallback: () => void) => void;
onFormChange?: (formData: Record<string, any>, isValid: boolean, tabId: string) => void;
}
export class ChatItemFormItemsWrapper {
private readonly props: ChatItemFormItemsWrapperProps;
private readonly options: Record<string, Select | TextArea | TextInput | RadioGroup | Stars | Checkbox> = {};
private readonly validationItems: Record<string, boolean> = {};
private isValid: boolean = false;
private tooltipOverlay: Overlay | null;
private tooltipTimeout: ReturnType<typeof setTimeout>;
onValidationChange?: (isValid: boolean) => void;
onAllFormItemsDisabled?: () => void;
render: ExtendedHTMLElement;
constructor (props: ChatItemFormItemsWrapperProps) {
this.props = props;
this.render = DomBuilder.getInstance().build({
type: 'div',
testId: testIds.chatItem.chatItemForm.wrapper,
classNames: [ 'mynah-chat-item-form-items-container', ...(this.props.classNames ?? []) ],
children: this.props.chatItem.formItems?.map(chatItemOption => {
let chatOption: Select | RadioGroup | TextArea | Stars | TextInput | Checkbox | undefined;
let label: ExtendedHTMLElement | string = `${chatItemOption.mandatory === true ? '* ' : ''}${chatItemOption.title ?? ''}`;
if (chatItemOption.mandatory === true) {
label = DomBuilder.getInstance().build({
type: 'div',
testId: testIds.chatItem.chatItemForm.title,
classNames: [ 'mynah-ui-form-item-mandatory-title' ],
children: [
new Icon({ icon: MynahIcons.ASTERISK }).render,
chatItemOption.title ?? '',
]
});
// Since the field is mandatory, default the selected value to the first option
if (chatItemOption.type === 'select' && chatItemOption.value === undefined) {
chatItemOption.value = chatItemOption.options?.[0]?.value;
}
}
let description;
if (chatItemOption.description != null) {
description = DomBuilder.getInstance().build({
type: 'span',
testId: testIds.chatItem.chatItemForm.description,
classNames: [ 'mynah-ui-form-item-description' ],
children: [
chatItemOption.description,
]
});
}
const fireModifierAndEnterKeyPress = (): void => {
if ((chatItemOption as TextBasedFormItem).checkModifierEnterKeyPress === true && this.isFormValid()) {
this.props.onModifierEnterPress?.(this.getAllValues(), props.tabId);
}
};
const value = chatItemOption.value?.toString();
switch (chatItemOption.type) {
case 'select':
chatOption = new Select({
wrapperTestId: testIds.chatItem.chatItemForm.itemSelectWrapper,
optionTestId: testIds.chatItem.chatItemForm.itemSelect,
label,
description,
value,
icon: chatItemOption.icon,
options: chatItemOption.options,
optional: chatItemOption.mandatory !== true,
placeholder: chatItemOption.placeholder ?? Config.getInstance().config.texts.pleaseSelect,
...(this.getHandlers(chatItemOption))
});
break;
case 'radiogroup':
case 'toggle':
chatOption = new RadioGroup({
type: chatItemOption.type,
wrapperTestId: testIds.chatItem.chatItemForm.itemRadioWrapper,
optionTestId: testIds.chatItem.chatItemForm.itemRadio,
label,
description,
value,
options: chatItemOption.options,
optional: chatItemOption.mandatory !== true,
...(this.getHandlers(chatItemOption))
});
break;
case 'checkbox':
chatOption = new Checkbox({
wrapperTestId: testIds.chatItem.chatItemForm.itemToggleWrapper,
optionTestId: testIds.chatItem.chatItemForm.itemToggleOption,
title: label,
label: chatItemOption.label,
icon: chatItemOption.icon,
description,
value: value as 'true' | 'false',
optional: chatItemOption.mandatory !== true,
...(this.getHandlers(chatItemOption))
});
break;
case 'switch':
chatOption = new Switch({
testId: testIds.chatItem.chatItemForm.itemSwitch,
title: label,
label: chatItemOption.label,
icon: chatItemOption.icon,
description,
value: value as 'true' | 'false',
optional: chatItemOption.mandatory !== true,
...(this.getHandlers(chatItemOption))
});
break;
case 'textarea':
chatOption = new TextArea({
testId: testIds.chatItem.chatItemForm.itemTextArea,
label,
autoFocus: chatItemOption.autoFocus,
description,
fireModifierAndEnterKeyPress,
onKeyPress: (event) => {
this.handleTextualItemKeyPressEvent(event, chatItemOption.id);
},
value,
mandatory: chatItemOption.mandatory,
validationPatterns: chatItemOption.validationPatterns,
placeholder: chatItemOption.placeholder,
...(this.getHandlers(chatItemOption))
});
break;
case 'textinput':
chatOption = new TextInput({
testId: testIds.chatItem.chatItemForm.itemInput,
label,
autoFocus: chatItemOption.autoFocus,
description,
icon: chatItemOption.icon,
fireModifierAndEnterKeyPress,
onKeyPress: (event) => {
this.handleTextualItemKeyPressEvent(event, chatItemOption.id);
},
value,
mandatory: chatItemOption.mandatory,
validationPatterns: chatItemOption.validationPatterns,
placeholder: chatItemOption.placeholder,
...(this.getHandlers(chatItemOption))
});
break;
case 'numericinput':
chatOption = new TextInput({
testId: testIds.chatItem.chatItemForm.itemInput,
label,
autoFocus: chatItemOption.autoFocus,
description,
icon: chatItemOption.icon,
fireModifierAndEnterKeyPress,
onKeyPress: (event) => {
this.handleTextualItemKeyPressEvent(event, chatItemOption.id);
},
value,
mandatory: chatItemOption.mandatory,
validationPatterns: chatItemOption.validationPatterns,
type: 'number',
placeholder: chatItemOption.placeholder,
...(this.getHandlers(chatItemOption))
});
break;
case 'email':
chatOption = new TextInput({
testId: testIds.chatItem.chatItemForm.itemInput,
label,
autoFocus: chatItemOption.autoFocus,
description,
icon: chatItemOption.icon,
fireModifierAndEnterKeyPress,
onKeyPress: (event) => {
this.handleTextualItemKeyPressEvent(event, chatItemOption.id);
},
value,
mandatory: chatItemOption.mandatory,
validationPatterns: chatItemOption.validationPatterns,
type: 'email',
placeholder: chatItemOption.placeholder,
...(this.getHandlers(chatItemOption))
});
break;
case 'stars':
chatOption = new Stars({
wrapperTestId: testIds.chatItem.chatItemForm.itemStarsWrapper,
optionTestId: testIds.chatItem.chatItemForm.itemStars,
label,
description,
value,
...(this.getHandlers(chatItemOption))
});
break;
default:
break;
}
if (chatOption != null) {
this.options[chatItemOption.id] = chatOption;
if (chatItemOption.tooltip != null) {
chatOption.render.update({
events: {
mouseover: (e) => {
cancelEvent(e);
if (chatItemOption.tooltip != null && chatOption?.render != null) {
let tooltipToShow = chatItemOption.tooltip;
if ((chatItemOption.type === 'checkbox' || chatItemOption.type === 'switch') && chatItemOption.alternateTooltip != null && chatOption.getValue() === 'false') {
tooltipToShow = chatItemOption.alternateTooltip;
}
this.showTooltip(tooltipToShow, chatOption.render);
}
},
mouseleave: this.hideTooltip
}
});
}
return chatOption.render;
}
return null;
}) as ExtendedHTMLElement[]
});
this.isFormValid();
}
private readonly showTooltip = (content: string, elm: HTMLElement | ExtendedHTMLElement): void => {
if (content.trim() !== undefined) {
clearTimeout(this.tooltipTimeout);
this.tooltipTimeout = setTimeout(() => {
this.tooltipOverlay = new Overlay({
background: true,
closeOnOutsideClick: false,
referenceElement: elm,
dimOutside: false,
removeOtherOverlays: true,
verticalDirection: OverlayVerticalDirection.TO_TOP,
horizontalDirection: OverlayHorizontalDirection.START_TO_RIGHT,
children: [
new Card({
border: false,
children: [
new CardBody({
body: content
}).render
]
}).render
],
});
}, TOOLTIP_DELAY);
}
};
public readonly hideTooltip = (): void => {
clearTimeout(this.tooltipTimeout);
if (this.tooltipOverlay !== null) {
this.tooltipOverlay?.close();
this.tooltipOverlay = null;
}
};
private readonly getHandlers = (chatItemOption: ChatItemFormItem): Object => {
if (chatItemOption.mandatory === true ||
([ 'textarea', 'textinput', 'numericinput', 'email' ].includes(chatItemOption.type) && (chatItemOption as TextBasedFormItem).validationPatterns != null)) {
// Set initial validation status
this.validationItems[chatItemOption.id] = this.isItemValid(chatItemOption.value ?? '', chatItemOption);
return {
onChange: (value: string | number) => {
this.props.onFormChange?.(this.getAllValues(), this.isFormValid(), this.props.tabId);
this.validationItems[chatItemOption.id] = this.isItemValid(value.toString(), chatItemOption);
this.isFormValid();
}
};
}
return {
onChange: () => {
this.props.onFormChange?.(this.getAllValues(), this.isFormValid(), this.props.tabId);
}
};
};
private readonly handleTextualItemKeyPressEvent = (event: KeyboardEvent, itemId: string): void => {
this.isFormValid() && this.props.onTextualItemKeyPress?.(event, itemId, this.getAllValues(), this.props.tabId, this.disableAll);
};
private readonly isItemValid = (value: string, chatItemOption: ChatItemFormItem): boolean => {
let validationState = true;
if (chatItemOption.mandatory === true) {
validationState = isMandatoryItemValid(value ?? '');
}
if (((chatItemOption.type === 'textarea' || chatItemOption.type === 'textinput') && chatItemOption.validationPatterns != null)) {
validationState = validationState && isTextualFormItemValid(value ?? '', chatItemOption.validationPatterns ?? { patterns: [] }, chatItemOption.mandatory).isValid;
}
return validationState;
};
isFormValid = (): boolean => {
const currentValidationStatus = Object.keys(this.validationItems).reduce((prev, curr) => {
return prev && this.validationItems[curr];
}, true);
if (this.isValid !== currentValidationStatus && this.onValidationChange !== undefined) {
this.onValidationChange(currentValidationStatus);
}
this.isValid = currentValidationStatus;
return currentValidationStatus;
};
disableAll = (): void => {
Object.keys(this.options).forEach(chatOptionId => this.options[chatOptionId].setEnabled(false));
this.onAllFormItemsDisabled?.();
};
getAllValues = (): Record<string, string> => {
const valueMap: Record<string, string> = {};
Object.keys(this.options).forEach(chatOptionId => {
valueMap[chatOptionId] = this.options[chatOptionId].getValue();
});
return valueMap;
};
}