166 lines
7.2 KiB
TypeScript
166 lines
7.2 KiB
TypeScript
import type { ReactElement } from 'react'
|
|
import type { Inline, PropertyItem, PropertyRow } from './types'
|
|
import { ChevronDown, ChevronRight } from 'lucide-react'
|
|
import { Fragment } from 'react'
|
|
import { inlineText } from './segments'
|
|
import { SegmentText } from './SegmentText'
|
|
import { subRowsLayout, tableCells, tableTemplate } from './subRows'
|
|
import { TextPreview } from './TextPreview'
|
|
import { propertyItemId, propertyRowId } from './viewState'
|
|
|
|
/** 列表子行里除最后一列以外各列的宽度(字典的键),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
|
|
const CELL_WIDTHS = ['min(150px, 28%)', 'min(220px, 38%)']
|
|
|
|
function gridTemplate(count: number): string {
|
|
const fixed = Array.from({ length: count - 1 }, (_, i) => CELL_WIDTHS[i] ?? CELL_WIDTHS[CELL_WIDTHS.length - 1])
|
|
return [...fixed, 'minmax(0, 1fr)'].join(' ')
|
|
}
|
|
|
|
/** 眼下折叠着哪些属性行、子行,点折叠箭头时切换哪个。 */
|
|
interface Folding {
|
|
collapsed: ReadonlySet<string>
|
|
onToggleFold: (id: string) => void
|
|
}
|
|
|
|
/**
|
|
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
|
|
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
|
|
*/
|
|
function ItemHead({ id, cell, foldable, folding }: { id: string, cell: Inline[], foldable: boolean, folding: Folding }): ReactElement {
|
|
if (!foldable) {
|
|
return (
|
|
<div className="flex min-w-0 gap-1">
|
|
<span className="w-3.5 shrink-0" />
|
|
<p className="cr-code min-w-0">
|
|
<SegmentText segments={cell} />
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
const folded = folding.collapsed.has(id)
|
|
const Chevron = folded ? ChevronRight : ChevronDown
|
|
return (
|
|
<button
|
|
type="button"
|
|
className="group flex min-w-0 items-start gap-1 rounded-sm text-left"
|
|
aria-label={`${folded ? '展开' : '折叠'}${inlineText(cell)}`}
|
|
aria-expanded={!folded}
|
|
onClick={() => folding.onToggleFold(id)}
|
|
>
|
|
<span className="flex h-5 w-3.5 shrink-0 items-center">
|
|
<Chevron size={14} strokeWidth={1.8} className="text-[var(--vscode-descriptionForeground)] group-hover:text-[var(--vscode-foreground)]" />
|
|
</span>
|
|
<span className="cr-code min-w-0">
|
|
<SegmentText segments={cell} />
|
|
</span>
|
|
</button>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 展开在一行或一个子行(parentId)下面的一层子行:左边一条竖线,每个子行几列对齐。
|
|
* 带下一层的子行(字典 / 对象的值又是字典 / 对象),下一层展开在它下面、再往右缩进一层,默认展开。
|
|
*/
|
|
function PropertyItems({ parentId, items, folding }: { parentId: string, items: PropertyItem[], folding: Folding }): ReactElement {
|
|
const nested = items.some(item => (item.items?.length ?? 0) > 0)
|
|
return (
|
|
<div className="flex gap-2.5 pl-[5px]">
|
|
<div className="cr-guide w-px shrink-0" />
|
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
{items.map((item, index) => {
|
|
const id = propertyItemId(parentId, index)
|
|
const children = item.items ?? []
|
|
return (
|
|
<Fragment key={index}>
|
|
<div
|
|
className="cr-prop-item grid gap-x-3"
|
|
data-id={id}
|
|
style={{ gridTemplateColumns: gridTemplate(item.cells.length) }}
|
|
>
|
|
{item.cells.map((cell, i) => i === 0 && nested
|
|
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0} folding={folding} />
|
|
: (
|
|
<p key={i} className="cr-code min-w-0">
|
|
<SegmentText segments={cell} />
|
|
</p>
|
|
))}
|
|
</div>
|
|
{children.length > 0 && !folding.collapsed.has(id) && <PropertyItems parentId={id} items={children} folding={folding} />}
|
|
</Fragment>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 给了列名的子行(输入参数、数据表的字段)排成表格:第一行是表头(列名),下面每个子行一行;外框圆角,格子之间、行之间一条分隔线。
|
|
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。
|
|
*/
|
|
function PropertyTable({ parentId, columns, items }: { parentId: string, columns: string[], items: PropertyItem[] }): ReactElement {
|
|
return (
|
|
<div className="pl-[5px]">
|
|
<div className="cr-table" role="table" style={{ gridTemplateColumns: tableTemplate(columns.length) }}>
|
|
<div className="cr-table-head" role="row">
|
|
{columns.map((name, i) => <span key={i} className="cr-table-cell" role="columnheader">{name}</span>)}
|
|
</div>
|
|
{items.map((item, index) => (
|
|
<div key={index} className="cr-table-row" role="row" data-id={propertyItemId(parentId, index)}>
|
|
{tableCells(item, columns).map((cell, i) => (
|
|
<p key={i} className="cr-table-cell cr-code min-w-0" role="cell">
|
|
<SegmentText segments={cell} />
|
|
</p>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 函数、类、枚举、导入、常量、变量块和库规则的卡片的预览:属性名一列对齐,值按词性着色;输入参数、导入项、字典 / 对象的值下面展开子行,
|
|
* 给了列名的(输入参数、数据表的字段)排成表格,没给的排成列表。
|
|
*/
|
|
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
|
|
blockId: string
|
|
rows: PropertyRow[]
|
|
collapsed: ReadonlySet<string>
|
|
onToggleFold: (id: string) => void
|
|
}): ReactElement {
|
|
return (
|
|
<div className="flex flex-col gap-[3px]">
|
|
{rows.map((row, index) => {
|
|
const layout = subRowsLayout(row)
|
|
const id = propertyRowId(blockId, index)
|
|
const folded = collapsed.has(id)
|
|
const Chevron = folded ? ChevronRight : ChevronDown
|
|
return (
|
|
<Fragment key={index}>
|
|
<div className="cr-prop-row flex gap-3" data-id={id}>
|
|
{layout
|
|
? (
|
|
<button
|
|
type="button"
|
|
className="cr-prop-label group flex h-5 w-[76px] shrink-0 items-center gap-1 rounded-sm"
|
|
aria-label={`${folded ? '展开' : '折叠'}${row.label}`}
|
|
aria-expanded={!folded}
|
|
onClick={() => onToggleFold(id)}
|
|
>
|
|
<Chevron size={14} strokeWidth={1.8} className="shrink-0 text-[var(--vscode-descriptionForeground)] group-hover:text-[var(--vscode-foreground)]" />
|
|
{row.label}
|
|
</button>
|
|
)
|
|
: <span className="cr-prop-label flex h-5 w-[76px] shrink-0 items-center">{row.label}</span>}
|
|
<TextPreview text={row.value} className="min-w-0 flex-1" />
|
|
</div>
|
|
{!folded && layout === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} />}
|
|
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={{ collapsed, onToggleFold }} />}
|
|
</Fragment>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|