From 95ab88693c78d84bda7978063bdbbc2c9d67eed1 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Fri, 8 Aug 2025 00:34:10 -0400 Subject: [PATCH] feat: Add smooth slide-up animation to SelectionToolbox (#4832) --- src/components/graph/SelectionOverlay.vue | 13 ++- src/components/graph/SelectionToolbox.vue | 27 ++++++- .../element/useRetriggerableAnimation.ts | 80 +++++++++++++++++++ src/lib/litegraph/src/LGraphCanvas.ts | 9 ++- src/types/selectionOverlayTypes.ts | 9 +++ 5 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 src/composables/element/useRetriggerableAnimation.ts create mode 100644 src/types/selectionOverlayTypes.ts diff --git a/src/components/graph/SelectionOverlay.vue b/src/components/graph/SelectionOverlay.vue index d0b6e9eb6..3bc41574a 100644 --- a/src/components/graph/SelectionOverlay.vue +++ b/src/components/graph/SelectionOverlay.vue @@ -14,12 +14,13 @@ + * ``` + */ +export function useRetriggerableAnimation( + trigger?: WatchSource | Ref, + options: { + animateOnMount?: boolean + animationDelay?: number + } = {} +) { + const { animateOnMount = true, animationDelay = 0 } = options + + const shouldAnimate = ref(false) + + /** + * Retriggers the animation by removing and re-adding the animation class + */ + const retriggerAnimation = () => { + // Remove animation class + shouldAnimate.value = false + // Force browser reflow to ensure the class removal is processed + void document.body.offsetHeight + // Re-add animation class in the next frame + requestAnimationFrame(() => { + if (animationDelay > 0) { + setTimeout(() => { + shouldAnimate.value = true + }, animationDelay) + } else { + shouldAnimate.value = true + } + }) + } + + // Trigger animation on mount if requested + if (animateOnMount) { + onMounted(() => { + if (animationDelay > 0) { + setTimeout(() => { + shouldAnimate.value = true + }, animationDelay) + } else { + shouldAnimate.value = true + } + }) + } + + // Watch for trigger changes to retrigger animation + if (trigger) { + watch(trigger, () => { + retriggerAnimation() + }) + } + + return { + shouldAnimate, + retriggerAnimation + } +} diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index cbb644cbf..d7778d5a1 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -4130,6 +4130,7 @@ export class LGraphCanvas const selected = this.selectedItems if (!selected.size) return + const initialSelectionSize = selected.size let wasSelected: Positionable | undefined for (const sel of selected) { if (sel === keepSelected) { @@ -4170,8 +4171,12 @@ export class LGraphCanvas } } - this.state.selectionChanged = true - this.onSelectionChange?.(this.selected_nodes) + // Only set selectionChanged if selection actually changed + const finalSelectionSize = selected.size + if (initialSelectionSize !== finalSelectionSize) { + this.state.selectionChanged = true + this.onSelectionChange?.(this.selected_nodes) + } } /** @deprecated See {@link LGraphCanvas.deselectAll} */ diff --git a/src/types/selectionOverlayTypes.ts b/src/types/selectionOverlayTypes.ts new file mode 100644 index 000000000..ee370e274 --- /dev/null +++ b/src/types/selectionOverlayTypes.ts @@ -0,0 +1,9 @@ +import type { InjectionKey, Ref } from 'vue' + +export interface SelectionOverlayState { + visible: Readonly> + updateCount: Readonly> +} + +export const SelectionOverlayInjectionKey: InjectionKey = + Symbol('selectionOverlayState')