mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-30 04:50:04 +00:00
The third PR for managing display of widgets on subgraph nodes. This is the one that actually makes the functionality usable and user visible. Adds - A right-side modal for configuring which widgets are promoted, accessed by right click or selection toolbar - This menu allows for re-arranging widget order by dragging and dropping. - Indicators inside the subgraph for which widgets have been promoted. - Context menu options for promoting or demoting widget inside of a subgraph. <img width="767" height="694" alt="image" src="https://github.com/user-attachments/assets/4f78645d-7b26-48ba-8c49-78f4807e89e8" /> <img width="784" height="435" alt="image" src="https://github.com/user-attachments/assets/7005c730-a732-481e-befb-57019a8a31a7" /> Known issues - Some preview widgets are not added to a node until a draw operation occurs. The code does not yet have a way of determining which nodes should have draw operations forced to facilitate initial widget creation. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5826-Add-UI-code-for-configuring-subgraphNode-widgets-27c6d73d36508146accbf395e5bcd36a) by [Unito](https://www.unito.io)
333 lines
9.7 KiB
TypeScript
333 lines
9.7 KiB
TypeScript
/*
|
|
Original implementation:
|
|
https://github.com/TahaSh/drag-to-reorder
|
|
MIT License
|
|
|
|
Copyright (c) 2023 Taha Shashtari
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
*/
|
|
const styleElement = document.createElement('style')
|
|
styleElement.textContent = `
|
|
.draggable-item {
|
|
position: relative;
|
|
will-change: transform;
|
|
user-select: none;
|
|
}
|
|
.draggable-item.is-idle {
|
|
transition: 0.25s ease transform;
|
|
}
|
|
.draggable-item.is-draggable {
|
|
z-index: 10;
|
|
}
|
|
`
|
|
document.head.append(styleElement)
|
|
|
|
export class DraggableList extends EventTarget {
|
|
listContainer
|
|
// @ts-expect-error fixme ts strict error
|
|
draggableItem
|
|
// @ts-expect-error fixme ts strict error
|
|
pointerStartX
|
|
// @ts-expect-error fixme ts strict error
|
|
pointerStartY
|
|
// @ts-expect-error fixme ts strict error
|
|
scrollYMax
|
|
itemsGap = 0
|
|
items = []
|
|
itemSelector
|
|
handleClass = 'drag-handle'
|
|
off = []
|
|
offDrag = []
|
|
|
|
// @ts-expect-error fixme ts strict error
|
|
constructor(element, itemSelector) {
|
|
super()
|
|
this.listContainer = element
|
|
this.itemSelector = itemSelector
|
|
|
|
if (!this.listContainer) return
|
|
|
|
// @ts-expect-error fixme ts strict error
|
|
this.off.push(this.on(this.listContainer, 'mousedown', this.dragStart))
|
|
// @ts-expect-error fixme ts strict error
|
|
this.off.push(this.on(this.listContainer, 'touchstart', this.dragStart))
|
|
// @ts-expect-error fixme ts strict error
|
|
this.off.push(this.on(document, 'mouseup', this.dragEnd))
|
|
// @ts-expect-error fixme ts strict error
|
|
this.off.push(this.on(document, 'touchend', this.dragEnd))
|
|
}
|
|
|
|
getAllItems() {
|
|
if (!this.items?.length) {
|
|
this.items = Array.from(
|
|
this.listContainer.querySelectorAll(this.itemSelector)
|
|
)
|
|
this.items.forEach((element) => {
|
|
// @ts-expect-error fixme ts strict error
|
|
element.classList.add('is-idle')
|
|
})
|
|
}
|
|
return this.items
|
|
}
|
|
|
|
getIdleItems() {
|
|
return this.getAllItems().filter((item) =>
|
|
// @ts-expect-error fixme ts strict error
|
|
item.classList.contains('is-idle')
|
|
)
|
|
}
|
|
|
|
// @ts-expect-error fixme ts strict error
|
|
isItemAbove(item) {
|
|
return item.hasAttribute('data-is-above')
|
|
}
|
|
|
|
// @ts-expect-error fixme ts strict error
|
|
isItemToggled(item) {
|
|
return item.hasAttribute('data-is-toggled')
|
|
}
|
|
|
|
// @ts-expect-error fixme ts strict error
|
|
on(source, event, listener, options?) {
|
|
listener = listener.bind(this)
|
|
source.addEventListener(event, listener, options)
|
|
return () => source.removeEventListener(event, listener)
|
|
}
|
|
|
|
// @ts-expect-error fixme ts strict error
|
|
dragStart(e) {
|
|
if (e.target.classList.contains(this.handleClass)) {
|
|
this.draggableItem = e.target.closest(this.itemSelector)
|
|
}
|
|
|
|
if (!this.draggableItem) return
|
|
|
|
this.pointerStartX = e.clientX || e.touches[0].clientX
|
|
this.pointerStartY = e.clientY || e.touches[0].clientY
|
|
this.scrollYMax =
|
|
this.listContainer.scrollHeight - this.listContainer.clientHeight
|
|
|
|
this.setItemsGap()
|
|
this.initDraggableItem()
|
|
this.initItemsState()
|
|
|
|
// @ts-expect-error fixme ts strict error
|
|
this.offDrag.push(this.on(document, 'mousemove', this.drag))
|
|
this.offDrag.push(
|
|
// @ts-expect-error fixme ts strict error
|
|
this.on(document, 'touchmove', this.drag, { passive: false })
|
|
)
|
|
|
|
this.dispatchEvent(
|
|
new CustomEvent('dragstart', {
|
|
detail: {
|
|
element: this.draggableItem,
|
|
// @ts-expect-error fixme ts strict error
|
|
position: this.getAllItems().indexOf(this.draggableItem)
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
setItemsGap() {
|
|
if (this.getIdleItems().length <= 1) {
|
|
this.itemsGap = 0
|
|
return
|
|
}
|
|
|
|
const item1 = this.getIdleItems()[0]
|
|
const item2 = this.getIdleItems()[1]
|
|
|
|
// @ts-expect-error fixme ts strict error
|
|
const item1Rect = item1.getBoundingClientRect()
|
|
// @ts-expect-error fixme ts strict error
|
|
const item2Rect = item2.getBoundingClientRect()
|
|
|
|
this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top)
|
|
}
|
|
|
|
initItemsState() {
|
|
this.getIdleItems().forEach((item, i) => {
|
|
// @ts-expect-error fixme ts strict error
|
|
if (this.getAllItems().indexOf(this.draggableItem) > i) {
|
|
// @ts-expect-error fixme ts strict error
|
|
item.dataset.isAbove = ''
|
|
}
|
|
})
|
|
}
|
|
|
|
initDraggableItem() {
|
|
this.draggableItem.classList.remove('is-idle')
|
|
this.draggableItem.classList.add('is-draggable')
|
|
}
|
|
|
|
// @ts-expect-error fixme ts strict error
|
|
drag(e) {
|
|
if (!this.draggableItem) return
|
|
|
|
e.preventDefault()
|
|
|
|
const clientX = e.clientX || e.touches[0].clientX
|
|
const clientY = e.clientY || e.touches[0].clientY
|
|
|
|
const listRect = this.listContainer.getBoundingClientRect()
|
|
|
|
if (clientY > listRect.bottom) {
|
|
if (this.listContainer.scrollTop < this.scrollYMax) {
|
|
this.listContainer.scrollBy(0, 10)
|
|
this.pointerStartY -= 10
|
|
}
|
|
} else if (clientY < listRect.top && this.listContainer.scrollTop > 0) {
|
|
this.pointerStartY += 10
|
|
this.listContainer.scrollBy(0, -10)
|
|
}
|
|
|
|
const pointerOffsetX = clientX - this.pointerStartX
|
|
const pointerOffsetY = clientY - this.pointerStartY
|
|
|
|
this.updateIdleItemsStateAndPosition()
|
|
this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`
|
|
}
|
|
|
|
updateIdleItemsStateAndPosition() {
|
|
const draggableItemRect = this.draggableItem.getBoundingClientRect()
|
|
const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2
|
|
|
|
// Update state
|
|
this.getIdleItems().forEach((item) => {
|
|
// @ts-expect-error fixme ts strict error
|
|
const itemRect = item.getBoundingClientRect()
|
|
const itemY = itemRect.top + itemRect.height / 2
|
|
if (this.isItemAbove(item)) {
|
|
if (draggableItemY <= itemY) {
|
|
// @ts-expect-error fixme ts strict error
|
|
item.dataset.isToggled = ''
|
|
} else {
|
|
// @ts-expect-error fixme ts strict error
|
|
delete item.dataset.isToggled
|
|
}
|
|
} else {
|
|
if (draggableItemY >= itemY) {
|
|
// @ts-expect-error fixme ts strict error
|
|
item.dataset.isToggled = ''
|
|
} else {
|
|
// @ts-expect-error fixme ts strict error
|
|
delete item.dataset.isToggled
|
|
}
|
|
}
|
|
})
|
|
|
|
// Update position
|
|
this.getIdleItems().forEach((item) => {
|
|
if (this.isItemToggled(item)) {
|
|
const direction = this.isItemAbove(item) ? 1 : -1
|
|
// @ts-expect-error fixme ts strict error
|
|
item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`
|
|
} else {
|
|
// @ts-expect-error fixme ts strict error
|
|
item.style.transform = ''
|
|
}
|
|
})
|
|
}
|
|
|
|
dragEnd() {
|
|
if (!this.draggableItem) return
|
|
|
|
this.applyNewItemsOrder()
|
|
this.cleanup()
|
|
}
|
|
|
|
applyNewItemsOrder() {
|
|
const reorderedItems = []
|
|
|
|
let oldPosition = -1
|
|
this.getAllItems().forEach((item, index) => {
|
|
if (item === this.draggableItem) {
|
|
oldPosition = index
|
|
return
|
|
}
|
|
if (!this.isItemToggled(item)) {
|
|
reorderedItems[index] = item
|
|
return
|
|
}
|
|
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
|
|
reorderedItems[newIndex] = item
|
|
})
|
|
|
|
for (let index = 0; index < this.getAllItems().length; index++) {
|
|
const item = reorderedItems[index]
|
|
if (typeof item === 'undefined') {
|
|
reorderedItems[index] = this.draggableItem
|
|
}
|
|
}
|
|
|
|
reorderedItems.forEach((item) => {
|
|
this.listContainer.appendChild(item)
|
|
})
|
|
|
|
// @ts-expect-error fixme ts strict error
|
|
this.items = reorderedItems
|
|
|
|
this.dispatchEvent(
|
|
new CustomEvent('dragend', {
|
|
detail: {
|
|
element: this.draggableItem,
|
|
oldPosition,
|
|
newPosition: reorderedItems.indexOf(this.draggableItem)
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
cleanup() {
|
|
this.itemsGap = 0
|
|
this.items = []
|
|
this.unsetDraggableItem()
|
|
this.unsetItemState()
|
|
|
|
// @ts-expect-error fixme ts strict error
|
|
this.offDrag.forEach((f) => f())
|
|
this.offDrag = []
|
|
}
|
|
|
|
unsetDraggableItem() {
|
|
this.draggableItem.style = null
|
|
this.draggableItem.classList.remove('is-draggable')
|
|
this.draggableItem.classList.add('is-idle')
|
|
this.draggableItem = null
|
|
}
|
|
|
|
unsetItemState() {
|
|
this.getIdleItems().forEach((item) => {
|
|
// @ts-expect-error fixme ts strict error
|
|
delete item.dataset.isAbove
|
|
// @ts-expect-error fixme ts strict error
|
|
delete item.dataset.isToggled
|
|
// @ts-expect-error fixme ts strict error
|
|
item.style.transform = ''
|
|
})
|
|
}
|
|
|
|
dispose() {
|
|
// @ts-expect-error fixme ts strict error
|
|
this.off.forEach((f) => f())
|
|
}
|
|
}
|