Group the digits as you type in the ledger
-2000000 and -20000000 are the same shape at a glance, and the ledger deals in both. Display only: what leaves the input is always the raw string, so nothing upstream ever sees a comma. Partly-typed numbers survive intact -- "1." and "-" and "1.50" are all states on the way to a value, and reformatting them into something else mid-keystroke makes the field unusable. The caret is restored by counting digits rather than remembering an offset, since inserting a comma shifts every character after it and a remembered position lands one place off for the rest of the number. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b4f579c4b1
commit
50f4c50b3a
@ -11,7 +11,7 @@
|
||||
// flex-1 buttons grew to absurd sizes; everything here is fixed-width and
|
||||
// left-aligned instead.
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef, useLayoutEffect } from 'react'
|
||||
|
||||
const INPUT = 'border border-gray-200 rounded px-2 py-1 text-xs bg-white w-28 text-right font-mono tabular-nums'
|
||||
const TEXT = 'border border-gray-200 rounded px-2 py-1 text-xs bg-white w-40 font-mono'
|
||||
@ -259,11 +259,59 @@ function derive(current, edit, dp = 2) {
|
||||
return out
|
||||
}
|
||||
|
||||
// Grouped while you type, because -2000000 and -20000000 are the same shape at
|
||||
// a glance and the ledger deals in both.
|
||||
//
|
||||
// Formats for display only: what leaves here is always the raw string, so the
|
||||
// arithmetic upstream never sees a comma. A partly-typed number has to survive
|
||||
// intact -- "1." and "-" and "1.50" are all states on the way to a value, and
|
||||
// reformatting them into something else as you type makes the field unusable.
|
||||
function groupDigits(raw) {
|
||||
const str = String(raw ?? '')
|
||||
if (str === '' || str === '-') return str
|
||||
const neg = str.startsWith('-')
|
||||
const body = neg ? str.slice(1) : str
|
||||
const dot = body.indexOf('.')
|
||||
const whole = (dot === -1 ? body : body.slice(0, dot)).replace(/\D/g, '')
|
||||
const frac = dot === -1 ? null : body.slice(dot + 1).replace(/\D/g, '')
|
||||
if (whole === '' && frac === null) return neg ? '-' : ''
|
||||
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
return `${neg ? '-' : ''}${grouped}${dot === -1 ? '' : `.${frac}`}`
|
||||
}
|
||||
|
||||
function LedgerInput({ value, active, onChange, onFocus, suffix }) {
|
||||
const ref = useRef(null)
|
||||
const caret = useRef(null)
|
||||
const display = groupDigits(value)
|
||||
|
||||
// The commas shift every character after them, so a remembered offset lands
|
||||
// in the wrong place. Count digits instead -- those are what the caret is
|
||||
// actually sitting between -- and find that many digits into the new text.
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el || caret.current == null) return
|
||||
const wanted = caret.current
|
||||
caret.current = null
|
||||
let seen = 0, pos = display.length
|
||||
for (let i = 0; i < display.length; i++) {
|
||||
if (/[\d.-]/.test(display[i])) seen++
|
||||
if (seen === wanted) { pos = i + 1; break }
|
||||
}
|
||||
if (wanted === 0) pos = 0
|
||||
try { el.setSelectionRange(pos, pos) } catch {}
|
||||
}, [display])
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<input
|
||||
type="text" inputMode="decimal" value={value} onChange={e => onChange(e.target.value)}
|
||||
ref={ref}
|
||||
type="text" inputMode="decimal" value={display}
|
||||
onChange={e => {
|
||||
const el = e.target
|
||||
const upto = el.value.slice(0, el.selectionStart ?? el.value.length)
|
||||
caret.current = (upto.match(/[\d.-]/g) || []).length
|
||||
onChange(el.value.replace(/,/g, ''))
|
||||
}}
|
||||
onFocus={onFocus} placeholder="—"
|
||||
className={`border rounded px-2 py-0.5 text-xs w-24 text-right font-mono tabular-nums
|
||||
${active ? 'border-blue-400 bg-blue-50/40 text-gray-800' : 'border-gray-200 bg-white text-gray-700'}`} />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user