From 970f827828b2e488ad5bb2e8f1363fd38c5a6102 Mon Sep 17 00:00:00 2001 From: J-Sek Date: Sat, 31 Aug 2024 15:26:12 +0200 Subject: [PATCH] fix(nested): Prevent infinite loops when resolving path (#20390) closes #20389 Co-authored-by: jsek Co-authored-by: Kael --- .../vuetify/src/components/VList/VList.tsx | 3 +- .../src/components/VList/VListItem.tsx | 3 ++ .../vuetify/src/composables/nested/nested.ts | 17 ++++++++++- .../vuetify/src/labs/VTreeview/VTreeview.tsx | 16 ++-------- .../src/labs/VTreeview/VTreeviewItem.tsx | 29 +++++-------------- 5 files changed, 32 insertions(+), 36 deletions(-) diff --git a/packages/vuetify/src/components/VList/VList.tsx b/packages/vuetify/src/components/VList/VList.tsx index f5c492fd9eb..cbdc04b2cce 100644 --- a/packages/vuetify/src/components/VList/VList.tsx +++ b/packages/vuetify/src/components/VList/VList.tsx @@ -162,7 +162,7 @@ export const VList = genericComponent props.lines ? `v-list--${props.lines}-line` : undefined) const activeColor = toRef(props, 'activeColor') const baseColor = toRef(props, 'baseColor') @@ -288,6 +288,7 @@ export const VList = genericComponent()({ root, parent, openOnSelect, + id: uid, } = useNestedItem(id, false) const list = useList() const isActive = computed(() => @@ -368,6 +369,8 @@ export const VListItem = genericComponent()({ isSelected, list, select, + root, + id: uid, } }, }) diff --git a/packages/vuetify/src/composables/nested/nested.ts b/packages/vuetify/src/composables/nested/nested.ts index 5f80f4da090..c5b013a3de0 100644 --- a/packages/vuetify/src/composables/nested/nested.ts +++ b/packages/vuetify/src/composables/nested/nested.ts @@ -17,7 +17,7 @@ import { leafSelectStrategy, leafSingleSelectStrategy, } from './selectStrategies' -import { getCurrentInstance, getUid, propsFactory } from '@/util' +import { consoleError, getCurrentInstance, getUid, propsFactory } from '@/util' // Types import type { InjectionKey, PropType, Ref } from 'vue' @@ -76,6 +76,7 @@ type NestedProvide = { activate: (id: unknown, value: boolean, event?: Event) => void select: (id: unknown, value: boolean, event?: Event) => void openOnSelect: (id: unknown, value: boolean, event?: Event) => void + getPath: (id: unknown) => unknown[] } } @@ -98,6 +99,7 @@ export const emptyNested: NestedProvide = { activated: ref(new Set()), selected: ref(new Map()), selectedValues: ref([]), + getPath: () => [], }, } @@ -191,6 +193,8 @@ export const useNested = (props: NestedProps) => { const vm = getCurrentInstance('nested') + const nodeIds = new Set() + const nested: NestedProvide = { id: shallowRef(), root: { @@ -209,6 +213,15 @@ export const useNested = (props: NestedProps) => { return arr }), register: (id, parentId, isGroup) => { + if (nodeIds.has(id)) { + const path = getPath(id).join(' -> ') + const newPath = getPath(parentId).concat(id).join(' -> ') + consoleError(`Multiple nodes with the same ID\n\t${path}\n\t${newPath}`) + return + } else { + nodeIds.add(id) + } + parentId && id !== parentId && parents.value.set(id, parentId) isGroup && children.value.set(id, []) @@ -220,6 +233,7 @@ export const useNested = (props: NestedProps) => { unregister: id => { if (isUnmounted) return + nodeIds.delete(id) children.value.delete(id) const parent = parents.value.get(id) if (parent) { @@ -289,6 +303,7 @@ export const useNested = (props: NestedProps) => { }, children, parents, + getPath, }, } diff --git a/packages/vuetify/src/labs/VTreeview/VTreeview.tsx b/packages/vuetify/src/labs/VTreeview/VTreeview.tsx index d4079447da0..172deddbc10 100644 --- a/packages/vuetify/src/labs/VTreeview/VTreeview.tsx +++ b/packages/vuetify/src/labs/VTreeview/VTreeview.tsx @@ -86,24 +86,14 @@ export const VTreeview = genericComponent( const search = toRef(props, 'search') const { filteredItems } = useFilter(props, flatItems, search) const visibleIds = computed(() => { - if (!search.value) { - return null - } + if (!search.value) return null + const getPath = vListRef.value?.getPath + if (!getPath) return null return new Set(filteredItems.value.flatMap(item => { return [...getPath(item.props.value), ...getChildren(item.props.value)] })) }) - function getPath (id: unknown) { - const path: unknown[] = [] - let parent: unknown = id - while (parent != null) { - path.unshift(parent) - parent = vListRef.value?.parents.get(parent) - } - return path - } - function getChildren (id: unknown) { const arr: unknown[] = [] const queue = ((vListRef.value?.children.get(id) ?? []).slice()) diff --git a/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx b/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx index aaa6aa59b2b..c3a33eee6ba 100644 --- a/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx +++ b/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx @@ -9,7 +9,6 @@ import { VProgressCircular } from '@/components/VProgressCircular' // Composables import { IconValue } from '@/composables/icons' -import { useNestedItem } from '@/composables/nested/nested' import { useLink } from '@/composables/router' // Utilities @@ -35,20 +34,11 @@ export const VTreeviewItem = genericComponent()({ setup (props, { attrs, slots, emit }) { const link = useLink(props, attrs) - const rawId = computed(() => props.value === undefined ? link.href.value : props.value) const vListItemRef = ref() - const { - activate, - isActivated, - isGroupActivator, - root, - id, - } = useNestedItem(rawId, false) - const isActivatableGroupActivator = computed(() => - (root.activatable.value) && - isGroupActivator + (vListItemRef.value?.root.activatable.value) && + vListItemRef.value?.isGroupActivator ) const isClickable = computed(() => @@ -60,15 +50,11 @@ export const VTreeviewItem = genericComponent()({ function activateItem (e: MouseEvent | KeyboardEvent) { if ( !isClickable.value || - (!isActivatableGroupActivator.value && isGroupActivator) + (!isActivatableGroupActivator.value && vListItemRef.value?.isGroupActivator) ) return - if (root.activatable.value) { - if (isActivatableGroupActivator.value) { - activate(!isActivated.value, e) - } else { - vListItemRef.value?.activate(!vListItemRef.value?.isActivated, e) - } + if (vListItemRef.value?.root.activatable.value) { + vListItemRef.value?.activate(!vListItemRef.value?.isActivated, e) } } @@ -80,13 +66,14 @@ export const VTreeviewItem = genericComponent()({ return (