Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restrict input types to that get rendered to HTML to not generate HTML errors #29

Merged
merged 10 commits into from
Feb 12, 2024
172 changes: 84 additions & 88 deletions lib/components/NodeBaseInputField.tsx
Original file line number Diff line number Diff line change
@@ -1,101 +1,97 @@
import { CSSProperties, memo, ReactNode, useRef, useState } from 'react'
import { CSSProperties, ReactNode, useRef, useState } from 'react'
import { NodeInputConfig } from '../config'

type NodeBaseInputFieldProps = Pick<NodeInputConfig, 'valueType' | 'name'> & {
value: any
style?: CSSProperties
inputStyle?: CSSProperties
onChange?: (value: any) => void
onPointerDown?: (e: React.PointerEvent) => void
onPointerLeave?: (e: React.PointerEvent) => void
children?: ReactNode
}
type NodeBaseInputFieldProps = Pick<NodeInputConfig, 'name'> &
React.InputHTMLAttributes<any> & {
setValue: (value: any) => void
inputStyle?: CSSProperties
onChange?: (value: any) => void
children?: ReactNode
}

export const NodeBaseInputField = memo(
({
name,
valueType,
value,
style,
inputStyle,
onChange,
onPointerDown,
onPointerLeave,
children,
}: NodeBaseInputFieldProps) => {
const [_value, setValue] = useState(value)
const [labelVisible, setLabelVisible] = useState(true)
const ref = useRef<HTMLInputElement>(null)
export const NodeBaseInputField = ({
name,
value,
setValue,
style,
inputStyle,
onChange,
onPointerDown,
onPointerLeave,
children,
...props
}: NodeBaseInputFieldProps) => {
const [labelVisible, setLabelVisible] = useState(true)
const ref = useRef<HTMLInputElement>(null)

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setValue(e.target.value.trim())
}
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
}

function handleBlur() {
setLabelVisible(true)
if (onChange) onChange(_value)
}
function handleBlur() {
setLabelVisible(true)
if (onChange) onChange(value)
}

return (
<div
return (
<div
style={{
margin: '2px 0',
padding: '0 12px',
position: 'relative',
display: 'flex',
...style,
}}
>
{children}
<input
{...props}
ref={ref}
style={{
margin: '2px 0',
padding: '0 12px',
position: 'relative',
display: 'flex',
...style,
background: '#545555',
border: 'none',
borderRadius: 3,
padding: '3px 8px',
color: '#fff',
textShadow: '0 1px rgba(0,0,0,0.4)',
fontSize: '12px',
textAlign: labelVisible ? 'right' : 'left',
flex: 1,
width: '100%',
...inputStyle,
}}
value={value}
onChange={handleChange}
onFocus={() => setLabelVisible(false)}
onBlur={handleBlur}
onKeyDown={(e) => {
if (e.key === 'Enter') {
ref.current!.blur()
}
}}
>
{children}
<input
ref={ref}
type={valueType as string}
onPointerDown={onPointerDown}
onPointerLeave={onPointerLeave}
/>
{labelVisible ? (
<div
style={{
background: '#545555',
border: 'none',
borderRadius: 3,
padding: '3px 8px',
position: 'absolute',
color: '#fff',
textShadow: '0 1px rgba(0,0,0,0.4)',
fontSize: '12px',
textAlign: labelVisible ? 'right' : 'left',
flex: 1,
width: '100%',
...inputStyle,
zIndex: 1,
top: 3,
left: 20,
textShadow: '0 1px rgba(0,0,0,0.4)',
backgroundColor: '#545555',
paddingRight: 8,
}}
onChange={handleChange}
onFocus={() => setLabelVisible(false)}
onBlur={handleBlur}
onKeyDown={(e) => {
if (e.key === 'Enter') {
ref.current!.blur()
}
onClick={() => {
ref.current!.focus()
}}
onPointerDown={onPointerDown}
onPointerLeave={onPointerLeave}
value={_value}
/>
{labelVisible ? (
<div
style={{
position: 'absolute',
color: '#fff',
fontSize: '12px',
zIndex: 1,
top: 3,
left: 20,
textShadow: '0 1px rgba(0,0,0,0.4)',
backgroundColor: '#545555',
paddingRight: 8,
}}
onClick={() => {
ref.current!.focus()
}}
>
{name}
</div>
) : null}
</div>
)
},
)
>
{name}
</div>
) : null}
</div>
)
}
188 changes: 168 additions & 20 deletions lib/components/NodeInputField.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,175 @@
import { memo } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { NodeBaseInputField } from './NodeBaseInputField'
import { useNodeFieldValue } from '../hooks/node'
import { NodeInputConfig, ValueTypeConfig } from '../config'
import { BaseInputProps } from './inputs.ts'
import './NodeInputField.css'

type NodeInputFieldProps = BaseInputProps & NodeInputConfig & ValueTypeConfig

export const NodeInputField = memo(
({ onFocus, onBlur, isConstant, slots, ...props }: NodeInputFieldProps) => {
const Handle = slots?.Handle
const [value, setValue] = useNodeFieldValue(props.id, props.defaultValue)

return (
<NodeBaseInputField
value={value}
onChange={setValue}
onPointerDown={onFocus}
onPointerLeave={onBlur}
{...props}
>
{isConstant || !Handle ? null : <Handle />}
</NodeBaseInputField>
)
type NodeInputFieldProps = BaseInputProps &
Omit<NodeInputConfig, 'valueType'> &
ValueTypeConfig &
React.InputHTMLAttributes<any>

export const NodeInputField = ({
onFocus,
onBlur,
type,
isConstant,
slots,
...props
}: NodeInputFieldProps) => {
const Handle = slots?.Handle
const [value, setValue] = useNodeFieldValue(props.id, props.defaultValue)

return (
<NodeBaseInputField
value={value}
setValue={setValue}
type={type}
onChange={setValue}
onPointerDown={onFocus}
onPointerLeave={onBlur}
{...props}
>
{isConstant || !Handle ? null : <Handle />}
</NodeBaseInputField>
)
}

type NodeInputDecimalFieldProps = NodeInputFieldProps & {
precision?: number
}
export const NodeInputDecimalField = ({
precision = 3,
onFocus,
onBlur,
type,
isConstant,
slots,
...props
}: NodeInputDecimalFieldProps) => {
const Handle = slots?.Handle
const [value, setValue] = useNodeFieldValue<number>(
props.id,
props.defaultValue,
)
const [displayValue, setDisplayValue] = useState<string>('')

useEffect(() => {
setDisplayValue(convertToDecimal(value))
}, [value, precision])

function convertToDecimal(val: string | number) {
if (typeof val === 'string') {
val = parseFloat(val)
}
if (isNaN(val)) return Number(0).toFixed(precision)
return val.toFixed(precision)
}

const handleChange = useCallback(
(val: any) => {
// Allow only numeric input
setValue(Number(convertToDecimal(val))) // Update global state
setDisplayValue(convertToDecimal(val)) // Update local state
},
[value],
)

return (
<NodeBaseInputField
type="text"
value={displayValue}
setValue={setDisplayValue}
inputMode="decimal"
onChange={handleChange}
pattern="\d+\\.\d\d\d"
step={0.001}
{...props}
>
{isConstant || !Handle ? null : <Handle />}
</NodeBaseInputField>
)
}

type NodeInputTypedFieldProps = Omit<NodeInputFieldProps, 'type'>

export const NodeInputTextField = (
props: NodeInputTypedFieldProps & { maxlength?: number; minlength?: number },
) => {
return <NodeInputField type="text" {...props} />
}

export const NodeInputNumberField = (
props: NodeInputTypedFieldProps & { max?: number; min?: number },
) => {
return <NodeInputField type="number" {...props} />
}

export const NodeInputPasswordField = (
props: NodeInputTypedFieldProps & { maxlength?: number; minlength?: number },
) => {
return <NodeInputField type="password" {...props} />
}

export const NodeInputEmailField = (
props: NodeInputTypedFieldProps & { pattern?: string },
) => {
return <NodeInputField type="email" {...props} />
}

export const NodeInputColorField = (props: NodeInputTypedFieldProps) => {
return <NodeInputField type="color" {...props} />
}

export const NodeInputDateField = (
props: NodeInputTypedFieldProps & { max?: string; min?: string },
) => {
return <NodeInputField type="date" {...props} />
}

export const NodeInputDateTimeLocalField = (
props: NodeInputTypedFieldProps & { max?: string; min?: string },
) => {
return <NodeInputField type="datetime-local" {...props} />
}

export const NodeInputMonthField = (
props: NodeInputTypedFieldProps & { max?: string; min?: string },
) => {
return <NodeInputField type="month" {...props} />
}

export const NodeInputRangeField = (
props: NodeInputTypedFieldProps & {
max?: number
min?: number
step?: number
},
)
) => {
return <NodeInputField type="range" {...props} />
}

export const NodeInputTelField = (
props: NodeInputTypedFieldProps & { pattern?: string },
) => {
return <NodeInputField type="tel" {...props} />
}

export const NodeInputTimeField = (
props: NodeInputTypedFieldProps & { max?: string; min?: string },
) => {
return <NodeInputField type="time" {...props} />
}

export const NodeInputUrlField = (
props: NodeInputTypedFieldProps & { pattern?: string },
) => {
return <NodeInputField type="url" {...props} />
}

export const NodeInputWeekField = (
props: NodeInputTypedFieldProps & { max?: string; min?: string },
) => {
return <NodeInputField type="week" {...props} />
}
Loading
Loading