PromucFlow_constructor/app/client/src/components/designSystems/blueprint/InputComponent.tsx

508 lines
14 KiB
TypeScript
Raw Normal View History

import React from "react";
import styled from "styled-components";
import {
getBorderCSSShorthand,
IntentColors,
labelStyle,
} from "constants/DefaultTheme";
2019-11-25 05:07:27 +00:00
import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
2019-10-30 10:23:20 +00:00
import {
Intent,
NumericInput,
IconName,
InputGroup,
Button,
2019-10-31 05:28:11 +00:00
Label,
2020-01-14 09:50:42 +00:00
Classes,
ControlGroup,
2020-02-06 07:01:25 +00:00
TextArea,
2019-10-30 10:23:20 +00:00
} from "@blueprintjs/core";
import { InputType, InputTypes } from "widgets/InputWidget";
import { WIDGET_PADDING } from "constants/WidgetConstants";
2020-02-06 07:01:25 +00:00
import { Colors } from "constants/Colors";
2020-03-06 09:45:21 +00:00
import ErrorTooltip from "components/editorComponents/ErrorTooltip";
import _ from "lodash";
import {
createMessage,
INPUT_WIDGET_DEFAULT_VALIDATION_ERROR,
} from "constants/messages";
import Dropdown, { DropdownOption } from "components/ads/Dropdown";
import { CurrencyTypeOptions, CurrencyOptionProps } from "constants/Currency";
import Icon, { IconSize } from "components/ads/Icon";
2019-10-30 10:23:20 +00:00
/**
* All design system component specific logic goes here.
2020-03-06 09:45:21 +00:00
* Ex. Blueprint has a separate numeric input and text input so switching between them goes here
2019-10-30 10:23:20 +00:00
* Ex. To set the icon as currency, blue print takes in a set of defined types
* All generic logic like max characters for phone numbers should be 10, should go in the widget
*/
2020-12-24 04:32:25 +00:00
const InputComponentWrapper = styled((props) => (
2020-03-13 07:24:03 +00:00
<ControlGroup {..._.omit(props, ["hasError", "numeric"])} />
2020-03-06 09:45:21 +00:00
))<{
2020-03-13 07:24:03 +00:00
numeric: boolean;
2020-03-06 09:45:21 +00:00
multiline: string;
hasError: boolean;
allowCurrencyChange?: boolean;
inputType: InputType;
2020-03-06 09:45:21 +00:00
}>`
2020-01-14 09:50:42 +00:00
&&&& {
.currency-type-filter {
width: 40px;
height: 32px;
position: absolute;
display: inline-block;
left: 0;
z-index: 16;
svg {
path {
fill: ${(props) => props.theme.colors.icon?.hover};
}
}
}
2020-02-06 07:01:25 +00:00
.${Classes.INPUT} {
${(props) =>
props.inputType === InputTypes.CURRENCY &&
props.allowCurrencyChange &&
`
padding-left: 45px;`};
${(props) =>
props.inputType === InputTypes.CURRENCY &&
!props.allowCurrencyChange &&
`
padding-left: 35px;`};
2020-01-17 12:34:58 +00:00
box-shadow: none;
2020-03-06 09:45:21 +00:00
border: 1px solid;
border-color: ${({ hasError }) =>
hasError ? IntentColors.danger : Colors.GEYSER_LIGHT};
border-radius: 0;
2020-12-24 04:32:25 +00:00
height: ${(props) => (props.multiline === "true" ? "100%" : "inherit")};
2020-03-06 09:45:21 +00:00
width: 100%;
2020-12-24 04:32:25 +00:00
${(props) =>
2020-03-13 07:24:03 +00:00
props.numeric &&
`
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-right-width: 0px;
`}
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
2020-02-06 07:01:25 +00:00
&:active {
2020-03-06 09:45:21 +00:00
border-color: ${({ hasError }) =>
hasError ? IntentColors.danger : Colors.HIT_GRAY};
2020-02-06 07:01:25 +00:00
}
&:focus {
2020-03-06 09:45:21 +00:00
border-color: ${({ hasError }) =>
hasError ? IntentColors.danger : Colors.MYSTIC};
&:focus {
border: ${(props) => getBorderCSSShorthand(props.theme.borders[2])};
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.1rem rgba(0, 123, 255, 0.25);
}
2020-02-06 07:01:25 +00:00
}
2020-01-17 12:34:58 +00:00
}
2020-02-06 07:01:25 +00:00
.${Classes.INPUT_GROUP} {
2020-01-14 09:50:42 +00:00
display: block;
margin: 0;
}
2020-02-06 07:01:25 +00:00
.${Classes.CONTROL_GROUP} {
2020-01-14 09:50:42 +00:00
justify-content: flex-start;
}
height: 100%;
align-items: center;
label {
2020-01-28 08:21:22 +00:00
${labelStyle}
flex: 0 1 30%;
2020-02-06 07:01:25 +00:00
margin: 7px ${WIDGET_PADDING * 2}px 0 0;
text-align: right;
2020-02-06 07:01:25 +00:00
align-self: flex-start;
2020-03-06 09:45:21 +00:00
max-width: calc(30% - ${WIDGET_PADDING}px);
}
}
`;
const DropdownTriggerIconWrapper = styled.div`
height: 19px;
padding: 9px 5px 9px 12px;
width: 40px;
height: 19px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
line-height: 19px;
letter-spacing: -0.24px;
color: #090707;
`;
const CurrencyIconWrapper = styled.span`
height: 100%;
padding: 6px 4px 6px 12px;
width: 28px;
position: absolute;
left: 0;
z-index: 16;
font-size: 14px;
line-height: 19px;
letter-spacing: -0.24px;
color: #090707;
`;
interface CurrencyDropdownProps {
onCurrencyTypeChange: (code?: string) => void;
options: Array<DropdownOption>;
selected: DropdownOption;
allowCurrencyChange?: boolean;
}
function CurrencyTypeDropdown(props: CurrencyDropdownProps) {
if (!props.allowCurrencyChange) {
return (
<CurrencyIconWrapper>
{getSelectedItem(props.selected.value).id}
</CurrencyIconWrapper>
);
}
const dropdownTriggerIcon = (
<DropdownTriggerIconWrapper className="t--input-currency-change">
{getSelectedItem(props.selected.value).id}
<Icon name="downArrow" size={IconSize.XXS} />
</DropdownTriggerIconWrapper>
);
return (
<Dropdown
containerClassName="currency-type-filter"
dropdownHeight="195px"
dropdownTriggerIcon={dropdownTriggerIcon}
enableSearch
onSelect={props.onCurrencyTypeChange}
optionWidth="260px"
options={props.options}
searchPlaceholder="Search by currency or country"
selected={props.selected}
showLabelOnly
/>
);
}
const getSelectedItem = (currencyCountryCode?: string): DropdownOption => {
let selectedCurrency: CurrencyOptionProps | undefined = currencyCountryCode
? CurrencyTypeOptions.find((item: CurrencyOptionProps) => {
return item.code === currencyCountryCode;
})
: undefined;
if (!selectedCurrency) {
selectedCurrency = {
code: "US",
currency: "USD",
currency_name: "US Dollar",
label: "United States",
phone: "1",
symbol_native: "$",
};
}
return {
label: `${selectedCurrency.currency} - ${selectedCurrency.currency_name}`,
searchText: selectedCurrency.label,
value: selectedCurrency.code,
id: selectedCurrency.symbol_native,
};
};
const countryToFlag = (isoCode: string) => {
return typeof String.fromCodePoint !== "undefined"
? isoCode
.toUpperCase()
.replace(/./g, (char) =>
String.fromCodePoint(char.charCodeAt(0) + 127397),
)
: isoCode;
};
export const getCurrencyOptions = (): Array<DropdownOption> => {
return CurrencyTypeOptions.map((item: CurrencyOptionProps) => {
return {
leftElement: countryToFlag(item.code),
searchText: item.label,
label: `${item.currency} - ${item.currency_name}`,
value: item.code,
id: item.symbol_native,
};
});
};
2019-10-30 10:23:20 +00:00
class InputComponent extends React.Component<
InputComponentProps,
InputComponentState
> {
constructor(props: InputComponentProps) {
super(props);
this.state = { showPassword: false };
}
2020-03-06 09:45:21 +00:00
setFocusState = (isFocused: boolean) => {
this.props.onFocusChange(isFocused);
};
2020-02-06 07:01:25 +00:00
onTextChange = (
event:
| React.ChangeEvent<HTMLInputElement>
| React.ChangeEvent<HTMLTextAreaElement>,
) => {
2019-10-31 05:28:11 +00:00
this.props.onValueChange(event.target.value);
};
onNumberChange = (valueAsNum: number, valueAsString: string) => {
if (this.props.inputType === InputTypes.CURRENCY) {
const fractionDigits = this.props.decimalsInCurrency || 0;
const currentIndexOfDecimal = valueAsString.indexOf(".");
const indexOfDecimal = valueAsString.length - fractionDigits - 1;
if (
valueAsString.includes(".") &&
currentIndexOfDecimal <= indexOfDecimal
) {
let value = valueAsString.split(",").join("");
if (value) {
if (currentIndexOfDecimal !== indexOfDecimal) {
value = value.substr(0, currentIndexOfDecimal + fractionDigits + 1);
}
const locale = navigator.languages?.[0] || "en-US";
const formatter = new Intl.NumberFormat(locale, {
style: "decimal",
minimumFractionDigits: fractionDigits,
});
const formattedValue = formatter.format(parseFloat(value));
this.props.onValueChange(formattedValue);
} else {
this.props.onValueChange("");
}
} else {
this.props.onValueChange(valueAsString);
}
} else {
this.props.onValueChange(valueAsString);
}
2019-10-31 05:28:11 +00:00
};
isNumberInputType(inputType: InputType) {
return (
2019-11-05 05:09:50 +00:00
inputType === "INTEGER" ||
inputType === "NUMBER" ||
inputType === "CURRENCY"
2019-10-31 05:28:11 +00:00
);
}
getIcon(inputType: InputType) {
switch (inputType) {
case "PHONE_NUMBER":
return "phone";
case "SEARCH":
return "search";
case "EMAIL":
return "envelope";
default:
return undefined;
}
}
getType(inputType: InputType) {
switch (inputType) {
case "PASSWORD":
2020-01-14 09:50:42 +00:00
return this.state.showPassword ? "text" : "password";
2019-10-31 05:28:11 +00:00
case "EMAIL":
return "email";
case "SEARCH":
return "search";
default:
return "text";
}
}
onKeyDownTextArea = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const isEnterKey = e.key === "Enter" || e.keyCode === 13;
const { disableNewLineOnPressEnterKey } = this.props;
if (isEnterKey && disableNewLineOnPressEnterKey && !e.shiftKey) {
e.preventDefault();
}
if (typeof this.props.onKeyDown === "function") {
this.props.onKeyDown(e);
}
};
onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (typeof this.props.onKeyDown === "function") {
this.props.onKeyDown(e);
}
};
private numericInputComponent = () => {
const minorStepSize =
this.props.inputType === InputTypes.CURRENCY
? this.props.decimalsInCurrency || 0
: 0;
return (
<NumericInput
allowNumericCharactersOnly
className={this.props.isLoading ? "bp3-skeleton" : Classes.FILL}
disabled={this.props.disabled}
intent={this.props.intent}
leftIcon={
this.props.inputType === "PHONE_NUMBER"
? "phone"
: this.props.inputType !== InputTypes.CURRENCY
? this.props.leftIcon
: this.props.inputType === InputTypes.CURRENCY && (
<CurrencyTypeDropdown
allowCurrencyChange={this.props.allowCurrencyChange}
onCurrencyTypeChange={this.props.onCurrencyTypeChange}
options={getCurrencyOptions()}
selected={getSelectedItem(this.props.currencyCountryCode)}
/>
)
}
max={this.props.maxNum}
maxLength={this.props.maxChars}
min={this.props.minNum}
minorStepSize={
minorStepSize === 0 ? undefined : Math.pow(10, -1 * minorStepSize)
}
onBlur={() => this.setFocusState(false)}
onFocus={() => this.setFocusState(true)}
onKeyDown={this.onKeyDown}
onValueChange={this.onNumberChange}
placeholder={this.props.placeholder}
stepSize={minorStepSize === 0 ? this.props.stepSize : undefined}
type={this.props.inputType === "PHONE_NUMBER" ? "tel" : undefined}
value={this.props.value}
/>
);
};
2020-03-13 07:24:03 +00:00
private textAreaInputComponent = () => (
2020-02-06 07:01:25 +00:00
<TextArea
className={this.props.isLoading ? "bp3-skeleton" : ""}
2020-02-06 07:01:25 +00:00
disabled={this.props.disabled}
growVertically={false}
2020-02-06 07:01:25 +00:00
intent={this.props.intent}
maxLength={this.props.maxChars}
onBlur={() => this.setFocusState(false)}
2020-02-06 07:01:25 +00:00
onChange={this.onTextChange}
2020-03-06 09:45:21 +00:00
onFocus={() => this.setFocusState(true)}
onKeyDown={this.onKeyDownTextArea}
placeholder={this.props.placeholder}
style={{ resize: "none" }}
value={this.props.value}
2020-02-06 07:01:25 +00:00
/>
);
private textInputComponent = (isTextArea: boolean) =>
isTextArea ? (
2020-03-13 07:24:03 +00:00
this.textAreaInputComponent()
2020-02-06 07:01:25 +00:00
) : (
<InputGroup
className={this.props.isLoading ? "bp3-skeleton" : ""}
2020-02-06 07:01:25 +00:00
disabled={this.props.disabled}
intent={this.props.intent}
maxLength={this.props.maxChars}
onBlur={() => this.setFocusState(false)}
2020-02-06 07:01:25 +00:00
onChange={this.onTextChange}
onFocus={() => this.setFocusState(true)}
onKeyDown={this.onKeyDown}
placeholder={this.props.placeholder}
2020-02-06 07:01:25 +00:00
rightElement={
this.props.inputType === "PASSWORD" ? (
<Button
icon={"lock"}
onClick={() => {
this.setState({ showPassword: !this.state.showPassword });
}}
/>
) : (
undefined
)
}
type={this.getType(this.props.inputType)}
value={this.props.value}
2020-02-06 07:01:25 +00:00
/>
);
private renderInputComponent = (inputType: InputType, isTextArea: boolean) =>
this.isNumberInputType(inputType)
? this.numericInputComponent()
: this.textInputComponent(isTextArea);
2019-10-31 05:28:11 +00:00
2019-10-30 10:23:20 +00:00
render() {
return (
2020-03-06 09:45:21 +00:00
<InputComponentWrapper
allowCurrencyChange={this.props.allowCurrencyChange}
2020-03-06 09:45:21 +00:00
fill
hasError={this.props.isInvalid}
inputType={this.props.inputType}
2020-03-06 09:45:21 +00:00
multiline={this.props.multiline.toString()}
2020-03-13 07:24:03 +00:00
numeric={this.isNumberInputType(this.props.inputType)}
2020-03-06 09:45:21 +00:00
>
2020-01-28 11:46:04 +00:00
{this.props.label && (
2020-01-31 11:13:16 +00:00
<Label
className={
this.props.isLoading
? Classes.SKELETON
: Classes.TEXT_OVERFLOW_ELLIPSIS
}
>
{this.props.label}
2020-01-28 11:46:04 +00:00
</Label>
)}
2020-03-06 09:45:21 +00:00
<ErrorTooltip
isOpen={this.props.isInvalid && this.props.showError}
message={
this.props.errorMessage ||
createMessage(INPUT_WIDGET_DEFAULT_VALIDATION_ERROR)
2020-03-06 09:45:21 +00:00
}
>
{this.renderInputComponent(
this.props.inputType,
this.props.multiline,
)}
</ErrorTooltip>
</InputComponentWrapper>
2019-10-30 10:23:20 +00:00
);
}
}
export interface InputComponentState {
showPassword?: boolean;
}
export interface InputComponentProps extends ComponentProps {
2020-03-06 09:45:21 +00:00
value: string;
2019-10-31 05:28:11 +00:00
inputType: InputType;
2019-10-30 10:23:20 +00:00
disabled?: boolean;
intent?: Intent;
defaultValue?: string;
currencyCountryCode?: string;
noOfDecimals?: number;
allowCurrencyChange?: boolean;
decimalsInCurrency?: number;
2019-10-31 05:28:11 +00:00
label: string;
2019-10-30 10:23:20 +00:00
leftIcon?: IconName;
allowNumericCharactersOnly?: boolean;
fill?: boolean;
2019-10-31 05:28:11 +00:00
errorMessage?: string;
maxChars?: number;
2019-10-30 10:23:20 +00:00
maxNum?: number;
minNum?: number;
2019-10-31 05:28:11 +00:00
onValueChange: (valueAsString: string) => void;
onCurrencyTypeChange: (code?: string) => void;
2019-10-30 10:23:20 +00:00
stepSize?: number;
placeholder?: string;
2019-12-03 04:41:10 +00:00
isLoading: boolean;
2020-02-13 09:32:24 +00:00
multiline: boolean;
2020-03-06 09:45:21 +00:00
isInvalid: boolean;
showError: boolean;
onFocusChange: (state: boolean) => void;
disableNewLineOnPressEnterKey?: boolean;
onKeyDown?: (
e:
| React.KeyboardEvent<HTMLTextAreaElement>
| React.KeyboardEvent<HTMLInputElement>,
) => void;
2019-10-30 10:23:20 +00:00
}
export default InputComponent;