File size: 3,448 Bytes
766d85d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { useState, useEffect, useRef } from 'react'

interface InlineEditCellProps {
  value: string | number | null | undefined
  onSave: (value: string | number | null) => void
  type?: 'text' | 'number'
  style?: React.CSSProperties
  placeholder?: string
  decimals?: number
  locale: string
  editTooltip?: string
  readOnly?: boolean
}

export default function InlineEditCell({ value, onSave, type = 'text', style = {} as React.CSSProperties, placeholder = '', decimals = 2, locale, editTooltip, readOnly = false }: InlineEditCellProps) {
  const [editing, setEditing] = useState(false)
  const [editValue, setEditValue] = useState<string | number>(value ?? '')
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => { if (editing && inputRef.current) { inputRef.current.focus(); inputRef.current.select() } }, [editing])

  const save = () => {
    setEditing(false)
    let v: string | number | null = editValue
    if (type === 'number') { const p = parseFloat(String(editValue).replace(',', '.')); v = isNaN(p) ? null : p }
    if (v !== value) onSave(v)
  }

  const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
    if (type !== 'number') return
    e.preventDefault()
    let text = e.clipboardData.getData('text').trim()
    // Strip everything except digits, dots, commas, minus
    text = text.replace(/[^\d.,-]/g, '')
    // Remove all thousand separators (dots or commas before 3-digit groups), keep last separator as decimal
    const lastComma = text.lastIndexOf(',')
    const lastDot = text.lastIndexOf('.')
    const decimalPos = Math.max(lastComma, lastDot)
    if (decimalPos > -1) {
      const intPart = text.substring(0, decimalPos).replace(/[.,]/g, '')
      const decPart = text.substring(decimalPos + 1)
      text = intPart + '.' + decPart
    } else {
      text = text.replace(/[.,]/g, '')
    }
    setEditValue(text)
  }

  if (editing) {
    return <input ref={inputRef} type="text" inputMode={type === 'number' ? 'decimal' : 'text'} value={editValue}
      onChange={e => setEditValue(e.target.value)} onBlur={save} onPaste={handlePaste}
      onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') { setEditValue(value ?? ''); setEditing(false) } }}
      style={{ width: '100%', border: '1px solid var(--accent)', borderRadius: 4, padding: '4px 6px', fontSize: 13, outline: 'none', background: 'var(--bg-input)', color: 'var(--text-primary)', fontFamily: 'inherit', ...style }}
      placeholder={placeholder} />
  }

  const display = type === 'number' && value != null
    ? Number(value).toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals })
    : (value || '')

  return (
    <div onClick={() => { if (readOnly) return; setEditValue(value ?? ''); setEditing(true) }} title={readOnly ? undefined : editTooltip}
      style={{ cursor: readOnly ? 'default' : 'pointer', padding: '2px 4px', borderRadius: 4, minHeight: 22, display: 'flex', alignItems: 'center',
        justifyContent: style?.textAlign === 'center' ? 'center' : 'flex-start', transition: 'background 0.15s',
        color: display ? 'var(--text-primary)' : 'var(--text-faint)', fontSize: 13, ...style }}
      onMouseEnter={e => { if (!readOnly) e.currentTarget.style.background = 'var(--bg-hover)' }}
      onMouseLeave={e => { if (!readOnly) e.currentTarget.style.background = 'transparent' }}>
      {display || placeholder || '-'}
    </div>
  )
}