Skip to content

Commit

Permalink
fix(admin/components/tree/list): 存在选中项时默认展开其所属项
Browse files Browse the repository at this point in the history
  • Loading branch information
caixw committed Sep 11, 2024
1 parent aa2657e commit 65a8881
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 34 deletions.
39 changes: 39 additions & 0 deletions admin/src/components/tree/item.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2024 caixw
//
// SPDX-License-Identifier: MIT

import { expect, test } from 'vitest';

import { Item, findItems } from './item';

test('findItems', () => {
const items: Array<Item> = [
{ type: 'item', value: 'v1', label: 'v1' },
{ type: 'item', value: 'v2', label: 'v2' },
{ type: 'item', value: 'v3', label: 'v3' },
{ type: 'divider' },
{
type: 'group', label: 'group', items: [
{ type: 'item', value: 'v22', label: 'v22' },
{ type: 'divider' },
{
type: 'item', value: 'v23', label: 'v23', items: [
{ type: 'item', value: 'v233', label: 'v233' },
{
type: 'item', label: 'v234', items: [
{ type: 'item', value: 'v2341', label: 'v2341' },
{ type: 'item', value: 'v2343', label: 'v2343' },
]
},
]
},
]
},
];

expect(findItems(items, 'v2')).toEqual([1]);
expect(findItems(items, 'v22')).toEqual([4,0]);
expect(findItems(items, 'v2343')).toEqual([4,2,1,1]);
expect(findItems(items, 'not-exists')).toBeUndefined();
expect(findItems(items)).toBeUndefined();
});
34 changes: 34 additions & 0 deletions admin/src/components/tree/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,37 @@ export type Item = {
*/
accesskey?: string;
};

/**
* 从 items 中查找值为 value 的项
* @param items 被查找对象
* @param value 查找的对象
* @returns 如果找到了,返回 value 在 items 的索引值,如果嵌套层,则返回每一次的索引。
*/
export function findItems(items: Array<Item>, value?: Value): Array<number>|undefined {
if (value === undefined) {
return;
}

for(const [index,item] of items.entries()) {
switch (item.type) {
case 'group':
if (item.items && item.items.length > 0) {
const indexes = findItems(item.items, value);
if (indexes && indexes.length > 0) {
return [index, ...indexes];
}
}
continue;
case 'item':
if (item.items && item.items.length > 0) {
const indexes = findItems(item.items, value);
if (indexes && indexes.length > 0) {
return [index, ...indexes];
}
} else if (item.value === value) {
return [index];
}
}
}
}
4 changes: 2 additions & 2 deletions admin/src/components/tree/list/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export default function() {
</div>

<div class="w-80 mt-4">
<p>不指定 onchange</p>
<List selectedClass={selectedCls()} palette={palette()}>
<p>不指定 onchange,但是有默认值</p>
<List selectedClass={selectedCls()} palette={palette()} selected='v2341'>
{items}
</List>
</div>
Expand Down
71 changes: 40 additions & 31 deletions admin/src/components/tree/list/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,31 @@
//
// SPDX-License-Identifier: MIT

import { A } from '@solidjs/router';
import { A, useLocation } from '@solidjs/router';
import { createSignal, For, JSX, Match, mergeProps, Show, Switch } from 'solid-js';
import { Dynamic } from 'solid-js/web';

import { Divider } from '@/components/divider';
import type { Props as ContainerProps } from '@/components/tree/container';
import type { Item, Value } from '@/components/tree/item';
import { findItems, type Item, type Value } from '@/components/tree/item';

export interface Props extends ContainerProps {
/**
* 可点击的元素是否以 A 作为标签名
* 设置选中项的初始值
*
* NOTE: 该值为非响应属性。
*/
selected?: Value;

/**
* 可点击的元素是否以 {@link A} 作为标签名
*
* 如果为 true,那为 {@link Item#value} 将作为链接的值。
*
* NOTE: 如果此值为 true,且 {@link Props#selected} 为 undefined,
* 则会尝试从地址中获取相应的值。
*
* NOTE: 该值为非响应属性。
*/
anchor?: boolean;
}
Expand All @@ -23,47 +35,53 @@ const defaultProps: Readonly<Partial<Props>> = {
selectedClass: 'selected'
};

/**
* 列表组件
*/
export default function (props: Props): JSX.Element {
props = mergeProps(defaultProps, props);

const [selected, setSelected] = createSignal<Value>();
const [selected, setSelected] = createSignal<Value|undefined>(props.selected ?? (props.anchor ? useLocation().pathname : undefined));
const selectedIndexes = findItems(props.children, selected());

const Items = (p: { items: Array<Item>, indent: number }): JSX.Element => {
const All = (p: { items: Array<Item>, indent: number, selectedIndex: number }): JSX.Element => {
return <For each={p.items}>
{(item) => (
{(item, index) => (
<Switch>
<Match when={item.type === 'divider'}>
<Divider />
</Match>
<Match when={item.type === 'group'}>
<Group item={item} indent={p.indent} />
<p class="group">{(item as any).label}</p>
<All items={(item as any).items} indent={p.indent} selectedIndex={p.selectedIndex+1} />
</Match>
<Match when={item.type === 'item'}>
<I item={item} indent={p.indent} />
<Items item={item} indent={p.indent} selectedIndex={p.selectedIndex}
isOpen={!!selectedIndexes && index() === selectedIndexes[p.selectedIndex]} />
</Match>
</Switch>
)}
</For>;
};

// 渲染 type==item 的元素
const I = (p: { item: Item, indent: number }) => {
const Items = (p: { item: Item, indent: number, selectedIndex: number, isOpen: boolean }) => {
if (p.item.type !== 'item') {
throw 'item.type 只能是 item';
}

const [open, setOpen] = createSignal(false);
const [open] = createSignal(p.isOpen);

return <Switch>
<Match when={p.item.items && p.item.items.length > 0}>
<details onToggle={()=>setOpen(!open())} open={open()}>
<details open={open()}>
<summary style={{ 'padding-left': `calc(${p.indent} * var(--item-space))` }} class="item">
{p.item.label}
<span class="tail c--icon">{ open() ?'keyboard_arrow_up' : 'keyboard_arrow_down' }</span>
</summary>
<Show when={p.item.items}>
<menu>
<Items items={p.item.items as Array<Item>} indent={p.indent+1} />
<All items={p.item.items as Array<Item>} indent={p.indent+1} selectedIndex={p.selectedIndex+1} />
</menu>
</Show>
</details>
Expand All @@ -73,6 +91,13 @@ export default function (props: Props): JSX.Element {
activeClass={props.selectedClass}
href={props.anchor ? p.item.value?.toString() ?? '' : ''}
accessKey={p.item.accesskey}
style={{ 'padding-left': `calc(${p.indent} * var(--item-space))` }}
classList={{
'item': true,

// anchor 的类型定义在 activeClass 属性
[props.anchor ? '' : props.selectedClass!]: !!props.selectedClass && selected() === p.item.value
}}
onClick={()=>{
if (p.item.type !== 'item') { throw 'p.item.type 必须为 item'; }

Expand All @@ -84,34 +109,18 @@ export default function (props: Props): JSX.Element {
}

setSelected(p.item.value);
}} style={{ 'padding-left': `calc(${p.indent} * var(--item-space))` }} classList={{
'item': true,

// anchor 的类型定义在 activeClass 属性
[props.anchor ? '' : props.selectedClass!]: !!props.selectedClass && selected() === p.item.value
}}>
}}
>
{p.item.label}
</Dynamic>
</Match>
</Switch>;
};

// 渲染 type==group 的元素
const Group = (p: { item: Item, indent: number }): JSX.Element => {
if (p.item.type !== 'group') {
throw 'item.type 只能是 group';
}

return <>
<p class="group">{p.item.label}</p>
<Items items={p.item.items} indent={p.indent} />
</>;
};

return <menu role="menu" classList={{
'c--list': true,
[`palette--${props.palette}`]: !!props.palette
}}>
<Items items={props.children} indent={1} />
<All items={props.children} indent={1} selectedIndex={0} />
</menu>;
}
2 changes: 1 addition & 1 deletion admin/src/core/theme/breakpoints.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ test('Breakpoints.compare', () => {
});

test('breakpointsMedia', () => {
expect(breakpointsMedia.xs).toEqual(`(width >= ${breakpoints.xs}px)`);
expect(breakpointsMedia.xs).toEqual(`(width >= ${breakpoints.xs})`);
expect(breakpointsMedia.lg).toEqual('(width >= 1024px)');
});

0 comments on commit 65a8881

Please sign in to comment.