Compare commits

...

5 Commits

Author SHA1 Message Date
Rizumu Ayaka
b255fc033e fix: guard against invalid oldPosition in TabGlobalParameters reorder
Add early return when draggableItem is not found in getAllItems(),
consistent with TabSubgraphInputs and preventing splice(-1, 1).
2026-04-10 19:08:55 +08:00
Rizumu Ayaka
8ce5616c98 fix: reset isDragging on guaranteed dragEnd path instead of inside applyNewItemsOrder
Wrap the base dragEnd method to reset isDragging after cleanup,
ensuring the flag is always cleared even if applyNewItemsOrder
returns early (e.g. when draggableItem is not found).
2026-04-10 19:02:51 +08:00
Rizumu Ayaka
665b703ae7 Merge branch 'main' into rizumu/fix/properties-panel-drag-reorder-flicker 2026-04-09 17:23:30 +08:00
Rizumu Ayaka
d9fdfdb6c2 fix: restrict dragAxis to 'y' or 'both' in DraggableList component 2026-04-09 16:55:14 +08:00
Rizumu Ayaka
70aa476060 fix: properties panel drag reorder visual flicker 2026-04-09 16:39:02 +08:00
6 changed files with 89 additions and 21 deletions

View File

@@ -3,6 +3,10 @@ import { onBeforeUnmount, ref, useTemplateRef, watchPostEffect } from 'vue'
import { DraggableList } from '@/scripts/ui/draggableList'
const { dragAxis } = defineProps<{
dragAxis?: 'y' | 'both'
}>()
const modelValue = defineModel<T[]>({ required: true })
const draggableList = ref<DraggableList>()
const draggableItems = useTemplateRef('draggableItems')
@@ -13,7 +17,8 @@ watchPostEffect(() => {
if (!draggableItems.value?.children?.length) return
draggableList.value = new DraggableList(
draggableItems.value,
'.draggable-item'
'.draggable-item',
{ dragAxis }
)
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems = []

View File

@@ -31,6 +31,7 @@ const {
widgets: widgetsProp,
showLocateButton = false,
isDraggable = false,
isDragging = false,
hiddenFavoriteIndicator = false,
showNodeName = false,
parents = [],
@@ -43,6 +44,7 @@ const {
widgets: { widget: IBaseWidget; node: LGraphNode }[]
showLocateButton?: boolean
isDraggable?: boolean
isDragging?: boolean
hiddenFavoriteIndicator?: boolean
showNodeName?: boolean
/**
@@ -272,7 +274,7 @@ defineExpose({
ref="widgetsContainer"
class="relative space-y-2 rounded-lg px-4 pt-1"
>
<TransitionGroup name="list-scale">
<TransitionGroup :name="isDragging ? undefined : 'list-scale'">
<WidgetItem
v-for="{ widget, node } in widgets"
:key="getStableWidgetRenderKey(widget)"

View File

@@ -28,6 +28,7 @@ const { t } = useI18n()
const draggableList = ref<DraggableList | undefined>(undefined)
const sectionWidgetsRef = ref<{ widgetsContainer: HTMLElement }>()
const isSearching = ref(false)
const isDragging = ref(false)
const favoritedWidgets = computed(
() => favoritedWidgetsStore.validFavoritedWidgets
@@ -56,8 +57,29 @@ function setDraggableState() {
const container = sectionWidgetsRef.value?.widgetsContainer
if (isSearching.value || !container?.children?.length) return
draggableList.value = new DraggableList(container, '.draggable-item')
draggableList.value = new DraggableList(container, '.draggable-item', {
dragAxis: 'y'
})
draggableList.value.addEventListener('dragstart', () => {
isDragging.value = true
})
const baseDragEnd = draggableList.value.dragEnd
draggableList.value.dragEnd = function () {
baseDragEnd.call(this)
nextTick(() => {
isDragging.value = false
})
}
/**
* Override to skip the base class's DOM `appendChild` reorder, which breaks
* Vue's vdom tracking inside <TransitionGroup> fragments. Instead, only
* update reactive state and let Vue handle the DOM reconciliation.
* TransitionGroup's move animation is suppressed via the `isDragging` prop
* on SectionWidgets to prevent the FLIP "snap-back" effect.
*/
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems: HTMLElement[] = []
@@ -75,6 +97,8 @@ function setDraggableState() {
reorderedItems[newIndex] = item
})
if (oldPosition === -1) return
for (let index = 0; index < this.getAllItems().length; index++) {
const item = reorderedItems[index]
if (typeof item === 'undefined') {
@@ -85,11 +109,13 @@ function setDraggableState() {
const newPosition = reorderedItems.indexOf(
this.draggableItem as HTMLElement
)
const widgets = [...searchedFavoritedWidgets.value]
const [widget] = widgets.splice(oldPosition, 1)
widgets.splice(newPosition, 0, widget)
searchedFavoritedWidgets.value = widgets
favoritedWidgetsStore.reorderFavorites(widgets)
if (oldPosition !== newPosition) {
const widgets = [...searchedFavoritedWidgets.value]
const [widget] = widgets.splice(oldPosition, 1)
widgets.splice(newPosition, 0, widget)
searchedFavoritedWidgets.value = widgets
favoritedWidgetsStore.reorderFavorites(widgets)
}
}
}
@@ -131,6 +157,7 @@ function onCollapseUpdate() {
:label
:widgets="searchedFavoritedWidgets"
:is-draggable="!isSearching"
:is-dragging="isDragging"
hidden-favorite-indicator
show-node-name
enable-empty-state

View File

@@ -52,6 +52,7 @@ const isAllCollapsed = computed({
}
})
const draggableList = ref<DraggableList | undefined>(undefined)
const isDragging = ref(false)
const sectionWidgetsRef = useTemplateRef('sectionWidgetsRef')
const advancedInputsSectionRef = useTemplateRef('advancedInputsSectionRef')
@@ -155,8 +156,29 @@ function setDraggableState() {
const container = sectionWidgetsRef.value?.widgetsContainer
if (isSearching.value || !container?.children?.length) return
draggableList.value = new DraggableList(container, '.draggable-item')
draggableList.value = new DraggableList(container, '.draggable-item', {
dragAxis: 'y'
})
draggableList.value.addEventListener('dragstart', () => {
isDragging.value = true
})
const baseDragEnd = draggableList.value.dragEnd
draggableList.value.dragEnd = function () {
baseDragEnd.call(this)
nextTick(() => {
isDragging.value = false
})
}
/**
* Override to skip the base class's DOM `appendChild` reorder, which breaks
* Vue's vdom tracking inside <TransitionGroup> fragments. Instead, only
* update reactive state and let Vue handle the DOM reconciliation.
* TransitionGroup's move animation is suppressed via the `isDragging` prop
* on SectionWidgets to prevent the FLIP "snap-back" effect.
*/
draggableList.value.applyNewItemsOrder = function () {
const reorderedItems: HTMLElement[] = []
@@ -189,14 +211,15 @@ function setDraggableState() {
const newPosition = reorderedItems.indexOf(
this.draggableItem as HTMLElement
)
promotionStore.movePromotion(
node.rootGraph.id,
node.id,
oldPosition,
newPosition
)
canvasStore.canvas?.setDirty(true, true)
if (oldPosition !== newPosition) {
promotionStore.movePromotion(
node.rootGraph.id,
node.id,
oldPosition,
newPosition
)
canvasStore.canvas?.setDirty(true, true)
}
}
}
@@ -236,6 +259,7 @@ const label = computed(() => {
:parents
:widgets="searchedWidgetsList"
:is-draggable="!isSearching"
:is-dragging="isDragging"
:enable-empty-state="isSearching"
:tooltip="
isSearching || searchedWidgetsList.length

View File

@@ -259,7 +259,11 @@ onMounted(() => {
{{ $t('subgraphStore.hideAll') }}</a
>
</div>
<DraggableList v-slot="{ dragClass }" v-model="activeWidgets">
<DraggableList
v-slot="{ dragClass }"
v-model="activeWidgets"
drag-axis="y"
>
<SubgraphNodeWidget
v-for="[node, widget] in filteredActive"
:key="toKey([node, widget])"

View File

@@ -53,14 +53,19 @@ export class DraggableList extends EventTarget {
items = []
itemSelector
handleClass = 'drag-handle'
dragAxis: 'y' | 'both' = 'both'
off = []
offDrag = []
// @ts-expect-error fixme ts strict error
constructor(element, itemSelector) {
constructor(
element: HTMLElement,
itemSelector: string,
options?: { dragAxis?: 'y' | 'both' }
) {
super()
this.listContainer = element
this.itemSelector = itemSelector
if (options?.dragAxis) this.dragAxis = options.dragAxis
if (!this.listContainer) return
@@ -203,7 +208,8 @@ export class DraggableList extends EventTarget {
this.listContainer.scrollBy(0, -10)
}
const pointerOffsetX = clientX - this.pointerStartX
const pointerOffsetX =
this.dragAxis === 'y' ? 0 : clientX - this.pointerStartX
const pointerOffsetY = clientY - this.pointerStartY
this.updateIdleItemsStateAndPosition()