From b2338727499560612a31955236131d633fa391b1 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 25 Nov 2024 16:16:00 +0200 Subject: [PATCH] filter-tree --- packages/radix-vue/src/Tree/TreeRoot.vue | 67 ++++-------- packages/radix-vue/src/Tree/utils.test.ts | 45 ++++++++ packages/radix-vue/src/Tree/utils.ts | 127 +++++++++++++++++++++- 3 files changed, 192 insertions(+), 47 deletions(-) create mode 100644 packages/radix-vue/src/Tree/utils.test.ts diff --git a/packages/radix-vue/src/Tree/TreeRoot.vue b/packages/radix-vue/src/Tree/TreeRoot.vue index 573af7f87..4612af589 100644 --- a/packages/radix-vue/src/Tree/TreeRoot.vue +++ b/packages/radix-vue/src/Tree/TreeRoot.vue @@ -55,20 +55,6 @@ interface TreeRootContext> { handleMultipleReplace: ReturnType['handleMultipleReplace'] } -export type FlattenedItem = { - _id: string - index: number - value: T - level: number - hasChildren: boolean - parentItem?: T - bind: { - value: T - level: number - [key: string]: any - } -} - export const [injectTreeRootContext, provideTreeRootContext] = createContext>('TreeRoot') @@ -78,6 +64,7 @@ import { type EventHook, createEventHook, useVModel } from '@vueuse/core' import { RovingFocusGroup } from '@/RovingFocus' import { type Ref, computed, nextTick, ref, toRefs } from 'vue' import { MAP_KEY_TO_FOCUS_INTENT } from '@/RovingFocus/utils' +import { flattenedItems, flattenItems, flattenFilter } from './utils' const props = withDefaults(defineProps>(), { as: 'ul', @@ -126,39 +113,29 @@ const selectedKeys = computed(() => { return [props.getKey(modelValue.value as any ?? {})] }) -function flattenItems(items: T[], level: number = 1, parentItem?: T): FlattenedItem[] { - return items.reduce((acc: FlattenedItem[], item: T, index: number) => { - const key = props.getKey(item) - const children = props.getChildren(item) - const isExpanded = expanded.value.includes(key) - - const flattenedItem: FlattenedItem = { - _id: key, - value: item, - index, - level, - parentItem, - hasChildren: !!children, - bind: { - 'value': item, - level, - 'aria-setsize': items.length, - 'aria-posinset': index + 1, - }, - } - acc.push(flattenedItem) - - if (children && isExpanded) - acc.push(...flattenItems(children, level + 1, item)) - - return acc - }, []) -} +const stext = ref('') +const searchFunc : (T)=>bool = (i) ->props.getKey(item).toLowerCase().includes(stext.value) +const searchForceVisible = ref([]) const expandedItems = computed(() => { - const items = props.items - const expandedKeys = expanded.value.map(i => i) - return flattenItems(items ?? []) + const items = props.items ?? [] + const expandedKeys = expanded.value.map((i) => i) + // when no search + if (search.value === '') { + return flattenItems(items, { + expanded: expandedKeys, + getKey: props.getKey, + getChildren: props.getChildren, + }) + } else { + const st = search.value.toLowerCase() + return flattenFilter(items, { + predicate: searchFunc.value, + forceVisible: searchForceVisible.value, + getKey: props.getKey, + getChildren: props.getChildren, + }) + } }) function handleKeydown(event: KeyboardEvent) { diff --git a/packages/radix-vue/src/Tree/utils.test.ts b/packages/radix-vue/src/Tree/utils.test.ts new file mode 100644 index 000000000..9848e12c2 --- /dev/null +++ b/packages/radix-vue/src/Tree/utils.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' + +import { flattenFilter } from './utils' + +describe('Tree', () => { + describe('flattenFilter', () => { + it('should return the filtered items and expanded keys', () => { + const items = [ + { + name: 'a', + children: [ + { + name: 'b', + children: [{ name: 'c' }], + }, + { + name: 'd', + children: [], + }, + ], + }, + { + name: 'e', + children: [{ name: 'f' }], + }, + ] + + const predicate = (item: any) => ['c', 'e'].includes(item.name) + const getKey = (item: any) => item.name + + const result = flattenFilter(items, { + predicate, + getKey, + getChildren: (item: any) => item.children, + forceVisible: [], + }) + + const names = result.result.map((item) => item._id) + expect(names).toEqual(['a', 'b', 'c', 'e']) + + expect(result.expanded.sort()).toEqual(['a', 'b']) + + }) + }) +}) \ No newline at end of file diff --git a/packages/radix-vue/src/Tree/utils.ts b/packages/radix-vue/src/Tree/utils.ts index bf1a7ba14..04c505597 100644 --- a/packages/radix-vue/src/Tree/utils.ts +++ b/packages/radix-vue/src/Tree/utils.ts @@ -1,4 +1,4 @@ -export function flatten(items: T[]): U[] { +export function flatten(items: T[]): U[] { return items.reduce((acc: any[], item: T) => { acc.push(item) @@ -9,4 +9,127 @@ export function flatten(items: T[]): U[] { }, []) } -// TODO: expose more utility function to handle flattened item +export type FlattenedItem = { + _id: string + index: number + value: T + level: number + hasChildren: boolean + parentItem?: T + bind: { + value: T + level: number + [key: string]: any + } +} + +export function flattenItems( + items: T[], + ctx: { + getKey: (item: T) => string + getChildren: (item: T) => T[] | undefined + expanded: string[] + }, +): FlattenedItem[] { + const expandedSet = new Set(ctx.expanded) + const flatNoFilter = (items: T[], level: number, parentItem?: T) => + items.reduce((acc: FlattenedItem[], item: T, index: number) => { + const key = ctx.getKey(item) + const children = ctx.getChildren(item) + + const flattenedItem: FlattenedItem = { + _id: key, + value: item, + index, + level, + parentItem, + hasChildren: !!children, + bind: { + value: item, + level, + 'aria-setsize': items.length, + 'aria-posinset': index + 1, + }, + } + acc.push(flattenedItem) + + if (children && expandedSet.has(key)) + acc.push(...flatNoFilter(children, level + 1, item)) + + return acc + }, []) + + return flatNoFilter(items, 1) +} + +export function flattenFilter( + items: T[], + ctx: { + getKey: (item: T) => string + getChildren: (item: T) => T[] | undefined + predicate: (item: T) => boolean + forceVisible: string[] + }, +): { + result: FlattenedItem[] + expanded: string[] +} { + const expanded: string[] = [] + const forceVisibleSet = new Set(ctx.forceVisible) + + const flatFilter = ( + itms: T[] | undefined, + force: boolean, + level: number, + parentItem?: T, + ) => + itms + ? itms.reduce((acc: FlattenedItem[], item: T, index: number) => { + const iMatch = force || ctx.predicate(item) + const key = ctx.getKey(item) + const children = ctx.getChildren(item) + //console.log('iMatch', iMatch, iKey, force) + + const cCnt = children ? children.length : 0 + + const flattenedItem: FlattenedItem = { + _id: key, + value: item, + index, + level, + parentItem, + hasChildren: cCnt > 0, + bind: { + value: item, + level, + 'aria-setsize': items.length, + 'aria-posinset': index + 1, + }, + } + + if (cCnt === 0) { + if (iMatch) acc.push(flattenedItem) + return acc + } + // check if children match, then add parent and add to expanded + const cres = flatFilter( + children, + forceVisibleSet.has(key), + level + 1, + item, + ) + if (cres.length > 0) { + acc.push(flattenedItem) + expanded.push(key) + acc.push(...cres) + } else if (iMatch) { + acc.push(flattenedItem) + } + + //console.log('return from flatFilter', acc) + return acc + }, []) + : [] + const result = flatFilter(items, true, 1, undefined) + return { result, expanded } +}