136 行
6.2 KiB
TypeScript
136 行
6.2 KiB
TypeScript
import { useId, useLayoutEffect, useRef, useState } from 'react'
|
|
|
|
import AnimatedCollapse from '@/components/common/AnimatedCollapse'
|
|
import SortableList from '@/components/common/SortableList'
|
|
import { countChildPaths, fieldsForCounts, normaliseCountsParams, orderedCountFields,
|
|
selectedCountChildren, selectedCountColumns, selectedCountFields } from '@/lib/counts'
|
|
import { useCountsDefinition } from '@/lib/countsDefinitions'
|
|
import { useClientMotion } from '@/lib/useClientMotion'
|
|
import { useUITranslation } from '@/lib/useUITranslation'
|
|
|
|
import type { CountField, CountsResource } from '@/lib/counts'
|
|
|
|
type BranchProps = {
|
|
field: CountField; path: string; enabled: boolean
|
|
selected: Set<string>; params: URLSearchParams
|
|
select: (path: string, checked: boolean) => void
|
|
update: (key: string, value: string) => void }
|
|
|
|
|
|
const FieldBranch = ({ field, path, enabled, selected, params, select, update }: BranchProps) => {
|
|
const { label, message } = useUITranslation ('counts')
|
|
const [open, setOpen] = useState (false)
|
|
const id = useId ()
|
|
const motion = useClientMotion ()
|
|
const input = useRef<HTMLInputElement> (null)
|
|
const leaves = countChildPaths (field.children ?? [], path, true)
|
|
const count = leaves.filter (key => selected.has (key)).length
|
|
const checked = field.children == null ? selected.has (path)
|
|
: count > 0 && count === leaves.length
|
|
const partial = count > 0 && count < leaves.length
|
|
useLayoutEffect (() => {
|
|
if (input.current != null) input.current.indeterminate = partial
|
|
}, [partial])
|
|
return <>
|
|
<div className="flex items-center gap-1">
|
|
<label className="inline-flex items-center gap-1">
|
|
<input ref={input} type="checkbox" checked={checked} disabled={!enabled}
|
|
aria-checked={partial ? 'mixed' : checked}
|
|
onChange={event => select (path, event.target.checked)}/>
|
|
{label (field.label, field.labelValues)}
|
|
</label>
|
|
{field.children != null && <button type="button" aria-expanded={open}
|
|
aria-controls={id} aria-label={message (field.label, field.labelValues).text}
|
|
lang={message (field.label, field.labelValues).language} onClick={() => setOpen (!open)}
|
|
className="rounded px-1 focus-visible:outline-none focus-visible:ring-2
|
|
focus-visible:ring-blue-500 dark:focus-visible:ring-blue-400">
|
|
<span aria-hidden className="inline-block" style={{
|
|
transform: open ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
transition: `transform ${motion.duration ('collapse')}ms ${motion.easing}` }}>▸</span>
|
|
</button>}
|
|
</div>
|
|
{field.children != null && <AnimatedCollapse open={open} id={id}>
|
|
<div className="ml-2 border-l pl-1 dark:border-slate-700">
|
|
<FieldGroup fields={field.children} path={path} params={params}
|
|
enabled={enabled} selected={selected} select={select} update={update}/>
|
|
</div>
|
|
</AnimatedCollapse>}
|
|
</>
|
|
}
|
|
|
|
|
|
const FieldGroup = ({ fields, path, params, enabled, selected, select, update }: {
|
|
fields: CountField[]; path: string; params: URLSearchParams
|
|
enabled: boolean; selected: Set<string>; select: BranchProps['select']
|
|
update: BranchProps['update'] },
|
|
) => {
|
|
const { message } = useUITranslation ('counts')
|
|
const ordered = orderedCountFields (fields, params, path)
|
|
const prefix = path ? `${path}.` : ''
|
|
return <SortableList groups={{ items: ordered.map (field => field.key) }}
|
|
fixed={path === '' ? ['thumbnail'] : []}
|
|
name={key => message ('reorder', {
|
|
column: message (fields.find (field => field.key === key)!.label,
|
|
fields.find (field => field.key === key)!.labelValues).text })}
|
|
renderGroup={(_, content) => content}
|
|
onChange={groups => {
|
|
const current = fields.map (field => `${prefix}${field.key}`)
|
|
const other = (params.get ('order') ?? '').split (' ')
|
|
.filter (key => key && !current.includes (key))
|
|
update ('order', [...other, ...groups.items.map (key => `${prefix}${key}`)].join (' '))
|
|
}} renderItem={key => {
|
|
const field = fields.find (field => field.key === key)!
|
|
return <FieldBranch field={field} path={`${prefix}${key}`} enabled={enabled}
|
|
selected={selected} params={params} select={select} update={update}/>
|
|
}}/>
|
|
}
|
|
|
|
|
|
const CountsFieldTree = (
|
|
{ resource, params, update, fields: suppliedFields,
|
|
selectionKey = 'columns', parent = '' }: {
|
|
resource: CountsResource
|
|
fields?: CountField[]
|
|
selectionKey?: string
|
|
parent?: string
|
|
params: URLSearchParams
|
|
update: (key: string, value: string) => void },
|
|
) => {
|
|
const definition = useCountsDefinition ()
|
|
const resolved = normaliseCountsParams (resource, params, definition)
|
|
const fields = suppliedFields ?? fieldsForCounts (resource, definition)
|
|
const roots = suppliedFields == null ? selectedCountColumns (resource, resolved, definition)
|
|
: selectedCountFields (fields, resolved, selectionKey, parent).map (field => field.key)
|
|
const selected = selectedCountChildren (resolved, fieldsForCounts (resource, definition))
|
|
const rootPath = (key: string) => parent ? `${parent}.${key}` : key
|
|
for (const root of roots) selected.add (rootPath (root))
|
|
const select = (path: string, checked: boolean) => {
|
|
const next = new Set (selected)
|
|
const paths = countChildPaths (fields, parent)
|
|
.filter (key => key === path || key.startsWith (`${path}.`))
|
|
for (const key of paths)
|
|
{
|
|
if (checked) next.add (key)
|
|
else next.delete (key)
|
|
}
|
|
const nextParams = new URLSearchParams (resolved)
|
|
nextParams.set ('children', [...next].filter (key => key.includes ('.')).join (' '))
|
|
const children = selectedCountChildren (nextParams, fieldsForCounts (resource, definition))
|
|
update ('children', [...children].join (' '))
|
|
update (selectionKey, fields.filter (field => field.children == null
|
|
? next.has (rootPath (field.key)) : field.children.some (child =>
|
|
children.has (`${rootPath (field.key)}.${child.key}`)))
|
|
.map (field => field.key).join (' '))
|
|
}
|
|
const visibleFields = parent === ''
|
|
? fields.filter (field => field.key !== 'thumbnail') : fields
|
|
return (
|
|
<>
|
|
<FieldGroup fields={visibleFields} path={parent} params={resolved} enabled selected={selected}
|
|
select={select} update={update}/>
|
|
</>)
|
|
}
|
|
|
|
|
|
export default CountsFieldTree
|