Files
fluxscape/packages/noodl-viewer-react/src/components/controls/TextInput/TextInput.tsx
Michael Cartner b9c60b07dc Initial commit
Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com>
Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com>
Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com>
Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com>
Co-Authored-By: Johan  <4934465+joolsus@users.noreply.github.com>
Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com>
Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
2024-01-26 11:52:55 +01:00

265 lines
7.0 KiB
TypeScript

import React from 'react';
import Layout from '../../../layout';
import Utils from '../../../nodes/controls/utils';
import { Noodl, Slot } from '../../../types';
//this stops a text field from being unfocused by the clickHandler in the viewer that handles focus globally.
//The specific case is when a mouseDown is registered in the input, but the mouseUp is outside.
//It'll trigger a focus change that'll blur the input field, which is annyoing when you're selecting text
function preventGlobalFocusChange(e) {
e.stopPropagation();
window.removeEventListener('click', preventGlobalFocusChange, true);
}
export interface TextInputProps extends Noodl.ReactProps {
id: string;
type: 'text' | 'textArea' | 'email' | 'number' | 'password' | 'url';
textStyle: Noodl.TextStyle;
enabled: boolean;
placeholder: string;
maxLength: number;
startValue: string;
value: string;
useLabel: boolean;
label: string;
labelSpacing: string;
labeltextStyle: Noodl.TextStyle;
useIcon: boolean;
iconPlacement: 'left' | 'right';
iconSpacing: string;
iconSourceType: 'image' | 'icon';
iconImageSource: Noodl.Image;
iconIconSource: Noodl.Icon;
iconSize: string;
iconColor: Noodl.Color;
onTextChanged?: (value: string) => void;
onEnter?: () => void;
children: Slot;
}
// Based on (HTMLTextAreaElement | HTMLInputElement)
type InputRef = (HTMLTextAreaElement | HTMLInputElement) & {
noodlNode?: Noodl.ReactProps['noodlNode'];
};
type State = {
value: string;
};
export class TextInput extends React.Component<TextInputProps, State> {
ref: React.MutableRefObject<InputRef>;
constructor(props: TextInputProps) {
super(props);
this.state = {
value: props.startValue
} satisfies State;
this.ref = React.createRef();
}
setText(value: string) {
this.setState({ value });
this.props.onTextChanged && this.props.onTextChanged(value);
}
componentDidMount() {
//plumbing for the focused signals
this.ref.current.noodlNode = this.props.noodlNode;
this.setText(this.props.startValue);
}
render() {
const style: React.CSSProperties = { ...this.props.style };
Layout.size(style, this.props);
Layout.align(style, this.props);
if (style.opacity === 0) {
style.pointerEvents = 'none';
}
const { height, ...otherStylesTmp } = style;
// otherStylesTmp is not React.CSSProperties, reassigning it will correct the type.
const otherStyles: React.CSSProperties = otherStylesTmp;
const props = this.props;
const _renderIcon = () => {
if (props.iconSourceType === 'image' && props.iconImageSource !== undefined)
return (
<img
alt=""
src={props.iconImageSource}
style={{
width: props.iconSize,
height: props.iconSize
}}
onClick={() => this.focus()}
/>
);
else if (props.iconSourceType === 'icon' && props.iconIconSource !== undefined) {
const style: React.CSSProperties = {
userSelect: 'none',
fontSize: props.iconSize,
color: props.iconColor
};
if (props.iconPlacement === 'left' || props.iconPlacement === undefined) style.marginRight = props.iconSpacing;
else style.marginLeft = props.iconSpacing;
if (props.iconIconSource.codeAsClass === true) {
return (
<span className={[props.iconIconSource.class, props.iconIconSource.code].join(' ')} style={style}></span>
);
} else {
return (
<span className={props.iconIconSource.class} style={style}>
{props.iconIconSource.code}
</span>
);
}
}
return null;
};
let className = 'ndl-controls-textinput ' + props.id;
if (props.className) className = className + ' ' + props.className;
let inputContent;
const inputStyles: React.CSSProperties = {
...props.textStyle,
...props.styles.input,
width: '100%',
height: '100%'
};
inputStyles.color = props.noodlNode.context.styles.resolveColor(inputStyles.color);
const inputProps = {
id: props.id,
value: this.state.value,
...Utils.controlEvents(props),
disabled: !props.enabled,
style: inputStyles,
className,
placeholder: props.placeholder,
maxLength: props.maxLength,
onChange: (e) => this.onChange(e)
};
if (props.type !== 'textArea') {
inputContent = (
<input
ref={(ref) => (this.ref.current = ref)}
type={this.props.type}
{...inputProps}
onKeyDown={(e) => this.onKeyDown(e)}
onMouseDown={() => window.addEventListener('click', preventGlobalFocusChange, true)}
noodl-style-tag="input"
/>
);
} else {
inputProps.style.resize = 'none'; //disable user resizing
inputContent = (
<textarea
ref={(ref) => (this.ref.current = ref)}
{...inputProps}
onKeyDown={(e) => this.onKeyDown(e)}
noodl-style-tag="input"
/>
);
}
const inputWrapperStyle = {
display: 'flex',
alignItems: 'center',
...props.styles.inputWrapper
};
const heightInPercent = height && height[String(height).length - 1] === '%';
if (props.useLabel) {
if (heightInPercent) {
inputWrapperStyle.flexGrow = 1;
} else {
inputWrapperStyle.height = height;
}
} else {
Object.assign(inputWrapperStyle, otherStyles);
inputWrapperStyle.height = height;
}
if (props.type !== 'textArea') {
inputWrapperStyle.alignItems = 'center';
}
const inputWithWrapper = (
<div style={inputWrapperStyle} noodl-style-tag="inputWrapper">
{props.useIcon && props.iconPlacement === 'left' ? _renderIcon() : null}
{inputContent}
{props.useIcon && props.iconPlacement === 'right' ? _renderIcon() : null}
</div>
);
if (props.useLabel) {
otherStyles.display = 'flex';
otherStyles.flexDirection = 'column';
if (heightInPercent) otherStyles.height = height;
const labelStyle: React.CSSProperties = {
...props.labeltextStyle,
...props.styles.label,
marginBottom: props.labelSpacing
};
labelStyle.color = props.noodlNode.context.styles.resolveColor(labelStyle.color);
return (
<div style={otherStyles}>
<label htmlFor={props.id} style={labelStyle} noodl-style-tag="label">
{props.label}
</label>
{inputWithWrapper}
</div>
);
} else {
return inputWithWrapper;
}
}
onKeyDown(e) {
if (e.key === 'Enter' || e.which === 13) {
this.props.onEnter && this.props.onEnter();
}
}
onChange(event) {
const value = event.target.value;
this.setText(value);
}
focus() {
this.ref.current && this.ref.current.focus();
}
blur() {
this.ref.current && this.ref.current.blur();
}
hasFocus() {
return document.activeElement === this.ref.current;
}
}