99 行
5.0 KiB
TypeScript
99 行
5.0 KiB
TypeScript
import LocalisedText from '@/components/LocalisedText'
|
||
import SortableList from '@/components/common/SortableList'
|
||
import { countsAxisFields, countsFilterFields, countsFieldLabel,
|
||
useCountsDefinition }
|
||
from '@/lib/countsDefinitions'
|
||
import { useClientMotion } from '@/lib/useClientMotion'
|
||
import { cn, inputClass } from '@/lib/utils'
|
||
import { useUITranslation } from '@/lib/useUITranslation'
|
||
|
||
import { readCountState, encodeCountState } from '@/lib/queryState'
|
||
|
||
import type { CountsAxis } from '@/lib/countsDefinitions'
|
||
|
||
|
||
const CountsAxes = ({ params, update }: {
|
||
params: URLSearchParams; update: (key: string, value: string) => void },
|
||
) => {
|
||
const { label, message } = useUITranslation ('counts')
|
||
const definition = useCountsDefinition ()
|
||
const motion = useClientMotion ()
|
||
const available = countsAxisFields (definition, params.get ('definition') === 'history')
|
||
const rows = readCountState<CountsAxis[]> (params, 'rows', [])
|
||
const cols = readCountState<CountsAxis[]> (params, 'cols', [])
|
||
const axes = [...rows, ...cols]
|
||
const groups = {
|
||
available: available.map (field => field.key).filter (id => !axes.some (
|
||
axis => axis.field === id)),
|
||
rows: rows.map (axis => axis.field), cols: cols.map (axis => axis.field) }
|
||
const name = (id: string) => {
|
||
const axis = axes.find (item => item.field === id)
|
||
return axis == null ? message (available.find (field => field.key === id)?.label ?? 'axis')
|
||
: message (countsFieldLabel (axis.field))
|
||
}
|
||
const commit = (next: Record<string, string[]>) => {
|
||
for (const group of ['rows', 'cols'] as const)
|
||
{
|
||
if (next[group].join (' ') === groups[group].join (' ')) continue
|
||
const selected = next[group].map (field => axes.find (axis => axis.field === field)
|
||
?? { field, ...(countsFilterFields (definition)[field] === 'datetime'
|
||
? { grain: 'month' } : { }) })
|
||
update (group, encodeCountState (group, selected))
|
||
}
|
||
}
|
||
const place = (id: string, target?: 'rows' | 'cols') => {
|
||
const next = Object.fromEntries (Object.entries (groups).map (([key, ids]) => [
|
||
key, ids.filter (value => value !== id)]))
|
||
if (target != null) next[target].push (id)
|
||
else next.available.push (id)
|
||
commit (next)
|
||
}
|
||
return <SortableList groups={groups} name={name} onChange={commit}
|
||
renderGroup={(id, content, over) => <fieldset className="mb-2 min-w-0">
|
||
<legend>{label (id === 'available' ? 'axisItems' : id)}</legend>
|
||
<div className={cn ('rounded border px-1 dark:border-slate-700',
|
||
over && 'ring-1 ring-blue-500 dark:ring-blue-400')}>{content}</div>
|
||
</fieldset>} renderItem={(id, group) => {
|
||
const axis = axes.find (axis => axis.field === id)
|
||
return <div className={cn ('flex items-center gap-2 text-sm',
|
||
group !== 'available' && 'flex-wrap')}>
|
||
<span className="min-w-0 flex-1 break-words">
|
||
<LocalisedText language={name (id).language}>{name (id).text}</LocalisedText>
|
||
</span>
|
||
{group === 'available' && <span className="ml-auto inline-flex shrink-0 gap-1">
|
||
{(['rows', 'cols'] as const).map (target => <button key={target} type="button"
|
||
className="h-7 w-7 rounded-full border border-slate-300 bg-slate-100
|
||
text-xs text-slate-700 hover:bg-blue-100 hover:text-blue-900
|
||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500
|
||
dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100
|
||
dark:hover:bg-blue-900 dark:hover:text-blue-100 dark:focus-visible:ring-blue-400"
|
||
style={{ transition: `background-color ${motion.duration ('feedback')}ms ${
|
||
motion.easing}, color ${motion.duration ('feedback')}ms ${motion.easing}` }}
|
||
aria-label={message ('axisPlace', {
|
||
field: name (id).text, target: message (target).text }).text}
|
||
lang={message ('axisPlace').language} onClick={() => place (id, target)}>
|
||
{label (target)}</button>)}
|
||
</span>}
|
||
{group !== 'available' && <>
|
||
{countsFilterFields (definition)[id] === 'datetime' && <select
|
||
className={inputClass (false, 'w-auto py-0')}
|
||
value={axis?.grain ?? 'month'} aria-label={message ('grain').text}
|
||
onChange={event => update (group, encodeCountState (group,
|
||
(group === 'rows' ? rows : cols).map (item => item.field === id
|
||
? { ...item, grain: event.target.value } : item)))}>
|
||
{definition.grains.map (grain => <option key={grain} value={grain}
|
||
lang={message (`grains.${grain}`).language}>
|
||
{message (`grains.${grain}`).text}</option>)}
|
||
</select>}
|
||
<button type="button" aria-label={message ('remove').text}
|
||
lang={message ('remove').language} className="rounded px-1
|
||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500
|
||
dark:focus-visible:ring-blue-400" onClick={() => place (id)}>×</button>
|
||
</>}
|
||
</div>
|
||
}}/>
|
||
}
|
||
|
||
|
||
export default CountsAxes
|