전략편집기 복합지표 기능 반영
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import React, { useEffect, useId, useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
options: number[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
allowDecimal?: boolean;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
function sanitizeDraft(raw: string, allowDecimal: boolean): string {
|
||||
if (allowDecimal) {
|
||||
let s = raw.replace(/[^\d.-]/g, '');
|
||||
const minus = s.startsWith('-') ? '-' : '';
|
||||
s = s.replace(/-/g, '');
|
||||
const dot = s.indexOf('.');
|
||||
if (dot !== -1) {
|
||||
s = s.slice(0, dot + 1) + s.slice(dot + 1).replace(/\./g, '');
|
||||
}
|
||||
return minus + s;
|
||||
}
|
||||
return raw.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
function parseDraft(raw: string, allowDecimal: boolean): number | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed || trimmed === '-' || trimmed === '.') return null;
|
||||
const n = allowDecimal ? parseFloat(trimmed) : parseInt(trimmed, 10);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
export default function ComboNumberInput({
|
||||
value,
|
||||
options,
|
||||
min = 1,
|
||||
max = 500,
|
||||
allowDecimal = false,
|
||||
onChange,
|
||||
}: Props) {
|
||||
const listId = useId().replace(/:/g, '');
|
||||
const [draft, setDraft] = useState(String(value));
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focused) setDraft(String(value));
|
||||
}, [value, focused]);
|
||||
|
||||
const commit = () => {
|
||||
const parsed = parseDraft(draft, allowDecimal);
|
||||
if (parsed == null) {
|
||||
setDraft(String(value));
|
||||
return;
|
||||
}
|
||||
const clamped = allowDecimal
|
||||
? Math.max(min, Math.min(max, parsed))
|
||||
: Math.max(min, Math.min(max, Math.round(parsed)));
|
||||
onChange(clamped);
|
||||
setDraft(String(clamped));
|
||||
};
|
||||
|
||||
const uniqueOptions = [...new Set([...options, value])].sort((a, b) => a - b);
|
||||
|
||||
return (
|
||||
<div className="se-combo-num">
|
||||
<input
|
||||
type="text"
|
||||
className="se-combo-num-input"
|
||||
inputMode={allowDecimal ? 'decimal' : 'numeric'}
|
||||
list={listId}
|
||||
value={draft}
|
||||
onChange={e => setDraft(sanitizeDraft(e.target.value, allowDecimal))}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => {
|
||||
setFocused(false);
|
||||
commit();
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<datalist id={listId}>
|
||||
{uniqueOptions.map(opt => (
|
||||
<option key={opt} value={String(opt)} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user