mirror of
https://github.com/fluxscape/fluxscape.git
synced 2026-01-11 23:02:55 +01:00
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>
265 lines
7.0 KiB
TypeScript
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;
|
|
}
|
|
}
|