diff --git a/src/info-panel/widget-editors/DragValueController.ts b/src/info-panel/widget-editors/DragValueController.ts index 2faefc7..126722c 100644 --- a/src/info-panel/widget-editors/DragValueController.ts +++ b/src/info-panel/widget-editors/DragValueController.ts @@ -26,8 +26,8 @@ export interface DragConfig { const DRAG_SENSITIVITY = { // Pixels to drag for one step change (applies to all numeric types) pixelsPerStep: 10, - // Minimum pixels to trigger option cycle for combos - comboThreshold: 40, + // Minimum pixels to trigger option cycle for combos (increased to prevent accidental cycling) + comboThreshold: 60, }; /** diff --git a/web/info-panel/widget-editors/DragValueController.js b/web/info-panel/widget-editors/DragValueController.js index e291021..ec6a9eb 100644 --- a/web/info-panel/widget-editors/DragValueController.js +++ b/web/info-panel/widget-editors/DragValueController.js @@ -6,8 +6,8 @@ import { Logger } from "../../shared/logger.js"; const DRAG_SENSITIVITY = { // Pixels to drag for one step change (applies to all numeric types) pixelsPerStep: 10, - // Minimum pixels to trigger option cycle for combos - comboThreshold: 40 + // Minimum pixels to trigger option cycle for combos (increased to prevent accidental cycling) + comboThreshold: 60 }; class DragValueController { constructor(element, config) { diff --git a/web/info-panel/widget-editors/DragValueController.js.map b/web/info-panel/widget-editors/DragValueController.js.map index 4e73bc5..f03d1d0 100644 --- a/web/info-panel/widget-editors/DragValueController.js.map +++ b/web/info-panel/widget-editors/DragValueController.js.map @@ -1 +1 @@ -{"version":3,"file":"DragValueController.js","sources":["../../../src/info-panel/widget-editors/DragValueController.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Drag Value Controller\n * \n * Handles horizontal drag gestures on inspector rows for quick value changes.\n * Allows users to drag left/right to adjust numbers or cycle through options.\n */\n\nimport { WidgetSyncManager, WidgetConstraints } from './WidgetSyncManager';\nimport { Logger } from '../../shared/logger';\n\n/**\n * Configuration for drag behavior\n */\nexport interface DragConfig {\n nodeId: number;\n widgetName: string;\n widgetType: string;\n currentValue: unknown;\n constraints?: WidgetConstraints;\n onChange?: (value: unknown) => void;\n}\n\n/**\n * Default sensitivity settings\n */\nconst DRAG_SENSITIVITY = {\n // Pixels to drag for one step change (applies to all numeric types)\n pixelsPerStep: 10,\n // Minimum pixels to trigger option cycle for combos\n comboThreshold: 40,\n};\n\n/**\n * Drag Value Controller\n * Manages drag interactions for inline value editing\n */\nexport class DragValueController {\n private element: HTMLElement;\n private config: DragConfig;\n private isDragging: boolean = false;\n private startX: number = 0;\n private startValue: number = 0;\n private startOptionIndex: number = 0;\n private accumulatedDelta: number = 0;\n\n // Bound event handlers for cleanup\n private boundPointerMove: (e: PointerEvent) => void;\n private boundPointerUp: (e: PointerEvent) => void;\n private boundPointerCancel: (e: PointerEvent) => void;\n private boundLostCapture: (e: PointerEvent) => void;\n\n constructor(element: HTMLElement, config: DragConfig) {\n this.element = element;\n this.config = config;\n\n this.boundPointerMove = this.onPointerMove.bind(this);\n this.boundPointerUp = this.onPointerUp.bind(this);\n this.boundPointerCancel = this.onPointerCancel.bind(this);\n this.boundLostCapture = this.onLostCapture.bind(this);\n\n this.init();\n }\n\n /**\n * Initialize drag handling\n */\n private init(): void {\n this.element.classList.add('draggable');\n\n this.element.addEventListener('pointerdown', this.onPointerDown.bind(this));\n\n // Add visual indicator\n // this.addDragIndicator();\n }\n\n /**\n * Add visual drag indicator to the row\n */\n private addDragIndicator(): void {\n // Check if already has indicator\n if (this.element.querySelector('.drag-indicator')) return;\n\n const indicator = document.createElement('span');\n indicator.className = 'drag-indicator';\n indicator.innerHTML = '⟷'; // Left-right arrow\n indicator.setAttribute('aria-hidden', 'true');\n this.element.appendChild(indicator);\n }\n\n /**\n * Handle pointer down - start drag tracking\n */\n private onPointerDown(e: PointerEvent): void {\n // Only handle left mouse button or touch\n if (e.button !== 0) return;\n\n // Don't start drag if clicking on interactive elements or value/editor areas\n // Only the label area should initiate drag - all controls should be clickable\n const target = e.target as HTMLElement;\n if (target.tagName === 'INPUT' ||\n target.tagName === 'SELECT' ||\n target.tagName === 'BUTTON' ||\n target.closest('.inline-control') ||\n target.closest('.inline-control-container') ||\n target.closest('.info-value') ||\n target.closest('.widget-editor') ||\n target.closest('.widget-editor-container') ||\n target.closest('.widget-editor-stepper') ||\n target.closest('.widget-editor-number') ||\n target.closest('.widget-editor-input')) {\n return;\n }\n\n this.isDragging = true;\n this.startX = e.clientX;\n this.accumulatedDelta = 0;\n\n // Capture current value\n const type = this.config.widgetType.toLowerCase();\n if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') {\n this.startValue = Number(this.config.currentValue) || 0;\n } else if (type === 'combo') {\n const options = this.config.constraints?.options ?? [];\n const currentStr = String(this.config.currentValue);\n this.startOptionIndex = options.findIndex(opt => String(opt) === currentStr);\n if (this.startOptionIndex === -1) this.startOptionIndex = 0;\n }\n\n // Add dragging class\n this.element.classList.add('dragging');\n\n // Capture pointer for reliable tracking\n this.element.setPointerCapture(e.pointerId);\n\n // Add document-level listeners with capture phase for reliability\n document.addEventListener('pointermove', this.boundPointerMove, true);\n document.addEventListener('pointerup', this.boundPointerUp, true);\n document.addEventListener('pointercancel', this.boundPointerCancel, true);\n this.element.addEventListener('lostpointercapture', this.boundLostCapture);\n\n e.preventDefault();\n e.stopPropagation();\n Logger.debug(`[DragValue] Started drag on ${this.config.widgetName}`);\n }\n\n /**\n * Handle pointer move - update value based on drag distance\n */\n private onPointerMove(e: PointerEvent): void {\n if (!this.isDragging) return;\n\n // Prevent browser from interpreting as text selection or native drag\n e.preventDefault();\n e.stopPropagation();\n\n const deltaX = e.clientX - this.startX;\n const type = this.config.widgetType.toLowerCase();\n\n if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') {\n this.handleNumberDrag(deltaX);\n } else if (type === 'combo') {\n this.handleComboDrag(deltaX);\n }\n }\n\n /**\n * Handle number value drag\n * Uses discrete stepping: every N pixels = exactly 1 step change\n */\n private handleNumberDrag(deltaX: number): void {\n const constraints = this.config.constraints;\n const isInt = this.config.widgetType.toLowerCase() === 'int';\n\n // Determine the step size (1 for integers, 0.1 or widget step for floats)\n const step = constraints?.step ?? (isInt ? 1 : 0.1);\n\n // Calculate discrete number of steps (truncate, don't round)\n const steps = Math.trunc(deltaX / DRAG_SENSITIVITY.pixelsPerStep);\n\n // Calculate new value\n let newValue = this.startValue + (steps * step);\n\n // Apply constraints\n if (constraints?.min !== undefined) {\n newValue = Math.max(constraints.min, newValue);\n }\n if (constraints?.max !== undefined) {\n newValue = Math.min(constraints.max, newValue);\n }\n\n // Round for display precision\n if (isInt) {\n newValue = Math.round(newValue);\n } else {\n const precision = constraints?.precision ?? 2;\n newValue = Number(newValue.toFixed(precision));\n }\n\n // Sync the value\n WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue);\n this.config.currentValue = newValue;\n this.config.onChange?.(newValue);\n }\n\n /**\n * Handle combo value drag - cycle through options\n */\n private handleComboDrag(deltaX: number): void {\n const options = this.config.constraints?.options ?? [];\n if (options.length === 0) return;\n\n this.accumulatedDelta = deltaX;\n\n // Calculate how many options to move\n const optionDelta = Math.floor(Math.abs(this.accumulatedDelta) / DRAG_SENSITIVITY.comboThreshold);\n\n if (optionDelta > 0) {\n const direction = deltaX > 0 ? 1 : -1;\n let newIndex = this.startOptionIndex + (optionDelta * direction);\n\n // Wrap around\n newIndex = ((newIndex % options.length) + options.length) % options.length;\n\n const newValue = options[newIndex];\n WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue);\n this.config.currentValue = newValue;\n this.config.onChange?.(newValue);\n }\n }\n\n /**\n * Handle pointer up - end drag\n */\n private onPointerUp(e: PointerEvent): void {\n if (!this.isDragging) return;\n this.endDrag(e.pointerId);\n Logger.debug(`[DragValue] Ended drag on ${this.config.widgetName}`);\n }\n\n /**\n * Handle pointer cancel - browser cancelled the pointer (e.g., touch scroll)\n */\n private onPointerCancel(e: PointerEvent): void {\n if (!this.isDragging) return;\n this.endDrag(e.pointerId);\n Logger.debug(`[DragValue] Drag cancelled on ${this.config.widgetName}`);\n }\n\n /**\n * Handle lost pointer capture - capture was taken by another element\n */\n private onLostCapture(_e: PointerEvent): void {\n if (!this.isDragging) return;\n this.endDrag();\n Logger.debug(`[DragValue] Lost capture on ${this.config.widgetName}`);\n }\n\n /**\n * Common cleanup for ending drag\n */\n private endDrag(pointerId?: number): void {\n this.isDragging = false;\n this.element.classList.remove('dragging');\n\n // Release pointer capture if we have a pointer ID\n if (pointerId !== undefined) {\n try {\n this.element.releasePointerCapture(pointerId);\n } catch {\n // Ignore if already released\n }\n }\n\n // Remove all listeners\n document.removeEventListener('pointermove', this.boundPointerMove, true);\n document.removeEventListener('pointerup', this.boundPointerUp, true);\n document.removeEventListener('pointercancel', this.boundPointerCancel, true);\n this.element.removeEventListener('lostpointercapture', this.boundLostCapture);\n }\n\n /**\n * Update the current value (call when value changes externally)\n */\n updateValue(value: unknown): void {\n this.config.currentValue = value;\n }\n\n /**\n * Cleanup and remove drag handling\n */\n destroy(): void {\n this.element.classList.remove('draggable', 'dragging');\n\n // Remove indicator\n const indicator = this.element.querySelector('.drag-indicator');\n if (indicator) {\n indicator.remove();\n }\n\n // Remove document listeners if still attached (with capture flag)\n document.removeEventListener('pointermove', this.boundPointerMove, true);\n document.removeEventListener('pointerup', this.boundPointerUp, true);\n document.removeEventListener('pointercancel', this.boundPointerCancel, true);\n this.element.removeEventListener('lostpointercapture', this.boundLostCapture);\n }\n\n /**\n * Check if a widget type supports drag editing\n */\n static isTypeSupported(widgetType: string): boolean {\n const type = widgetType.toLowerCase();\n return [\n 'number', 'int', 'float', 'slider', 'combo'\n ].includes(type);\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"DragValueController.js","sources":["../../../src/info-panel/widget-editors/DragValueController.ts"],"sourcesContent":["/**\n * ComfyUI MagnifyGlass - Drag Value Controller\n * \n * Handles horizontal drag gestures on inspector rows for quick value changes.\n * Allows users to drag left/right to adjust numbers or cycle through options.\n */\n\nimport { WidgetSyncManager, WidgetConstraints } from './WidgetSyncManager';\nimport { Logger } from '../../shared/logger';\n\n/**\n * Configuration for drag behavior\n */\nexport interface DragConfig {\n nodeId: number;\n widgetName: string;\n widgetType: string;\n currentValue: unknown;\n constraints?: WidgetConstraints;\n onChange?: (value: unknown) => void;\n}\n\n/**\n * Default sensitivity settings\n */\nconst DRAG_SENSITIVITY = {\n // Pixels to drag for one step change (applies to all numeric types)\n pixelsPerStep: 10,\n // Minimum pixels to trigger option cycle for combos (increased to prevent accidental cycling)\n comboThreshold: 60,\n};\n\n/**\n * Drag Value Controller\n * Manages drag interactions for inline value editing\n */\nexport class DragValueController {\n private element: HTMLElement;\n private config: DragConfig;\n private isDragging: boolean = false;\n private startX: number = 0;\n private startValue: number = 0;\n private startOptionIndex: number = 0;\n private accumulatedDelta: number = 0;\n\n // Bound event handlers for cleanup\n private boundPointerMove: (e: PointerEvent) => void;\n private boundPointerUp: (e: PointerEvent) => void;\n private boundPointerCancel: (e: PointerEvent) => void;\n private boundLostCapture: (e: PointerEvent) => void;\n\n constructor(element: HTMLElement, config: DragConfig) {\n this.element = element;\n this.config = config;\n\n this.boundPointerMove = this.onPointerMove.bind(this);\n this.boundPointerUp = this.onPointerUp.bind(this);\n this.boundPointerCancel = this.onPointerCancel.bind(this);\n this.boundLostCapture = this.onLostCapture.bind(this);\n\n this.init();\n }\n\n /**\n * Initialize drag handling\n */\n private init(): void {\n this.element.classList.add('draggable');\n\n this.element.addEventListener('pointerdown', this.onPointerDown.bind(this));\n\n // Add visual indicator\n // this.addDragIndicator();\n }\n\n /**\n * Add visual drag indicator to the row\n */\n private addDragIndicator(): void {\n // Check if already has indicator\n if (this.element.querySelector('.drag-indicator')) return;\n\n const indicator = document.createElement('span');\n indicator.className = 'drag-indicator';\n indicator.innerHTML = '⟷'; // Left-right arrow\n indicator.setAttribute('aria-hidden', 'true');\n this.element.appendChild(indicator);\n }\n\n /**\n * Handle pointer down - start drag tracking\n */\n private onPointerDown(e: PointerEvent): void {\n // Only handle left mouse button or touch\n if (e.button !== 0) return;\n\n // Don't start drag if clicking on interactive elements or value/editor areas\n // Only the label area should initiate drag - all controls should be clickable\n const target = e.target as HTMLElement;\n if (target.tagName === 'INPUT' ||\n target.tagName === 'SELECT' ||\n target.tagName === 'BUTTON' ||\n target.closest('.inline-control') ||\n target.closest('.inline-control-container') ||\n target.closest('.info-value') ||\n target.closest('.widget-editor') ||\n target.closest('.widget-editor-container') ||\n target.closest('.widget-editor-stepper') ||\n target.closest('.widget-editor-number') ||\n target.closest('.widget-editor-input')) {\n return;\n }\n\n this.isDragging = true;\n this.startX = e.clientX;\n this.accumulatedDelta = 0;\n\n // Capture current value\n const type = this.config.widgetType.toLowerCase();\n if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') {\n this.startValue = Number(this.config.currentValue) || 0;\n } else if (type === 'combo') {\n const options = this.config.constraints?.options ?? [];\n const currentStr = String(this.config.currentValue);\n this.startOptionIndex = options.findIndex(opt => String(opt) === currentStr);\n if (this.startOptionIndex === -1) this.startOptionIndex = 0;\n }\n\n // Add dragging class\n this.element.classList.add('dragging');\n\n // Capture pointer for reliable tracking\n this.element.setPointerCapture(e.pointerId);\n\n // Add document-level listeners with capture phase for reliability\n document.addEventListener('pointermove', this.boundPointerMove, true);\n document.addEventListener('pointerup', this.boundPointerUp, true);\n document.addEventListener('pointercancel', this.boundPointerCancel, true);\n this.element.addEventListener('lostpointercapture', this.boundLostCapture);\n\n e.preventDefault();\n e.stopPropagation();\n Logger.debug(`[DragValue] Started drag on ${this.config.widgetName}`);\n }\n\n /**\n * Handle pointer move - update value based on drag distance\n */\n private onPointerMove(e: PointerEvent): void {\n if (!this.isDragging) return;\n\n // Prevent browser from interpreting as text selection or native drag\n e.preventDefault();\n e.stopPropagation();\n\n const deltaX = e.clientX - this.startX;\n const type = this.config.widgetType.toLowerCase();\n\n if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') {\n this.handleNumberDrag(deltaX);\n } else if (type === 'combo') {\n this.handleComboDrag(deltaX);\n }\n }\n\n /**\n * Handle number value drag\n * Uses discrete stepping: every N pixels = exactly 1 step change\n */\n private handleNumberDrag(deltaX: number): void {\n const constraints = this.config.constraints;\n const isInt = this.config.widgetType.toLowerCase() === 'int';\n\n // Determine the step size (1 for integers, 0.1 or widget step for floats)\n const step = constraints?.step ?? (isInt ? 1 : 0.1);\n\n // Calculate discrete number of steps (truncate, don't round)\n const steps = Math.trunc(deltaX / DRAG_SENSITIVITY.pixelsPerStep);\n\n // Calculate new value\n let newValue = this.startValue + (steps * step);\n\n // Apply constraints\n if (constraints?.min !== undefined) {\n newValue = Math.max(constraints.min, newValue);\n }\n if (constraints?.max !== undefined) {\n newValue = Math.min(constraints.max, newValue);\n }\n\n // Round for display precision\n if (isInt) {\n newValue = Math.round(newValue);\n } else {\n const precision = constraints?.precision ?? 2;\n newValue = Number(newValue.toFixed(precision));\n }\n\n // Sync the value\n WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue);\n this.config.currentValue = newValue;\n this.config.onChange?.(newValue);\n }\n\n /**\n * Handle combo value drag - cycle through options\n */\n private handleComboDrag(deltaX: number): void {\n const options = this.config.constraints?.options ?? [];\n if (options.length === 0) return;\n\n this.accumulatedDelta = deltaX;\n\n // Calculate how many options to move\n const optionDelta = Math.floor(Math.abs(this.accumulatedDelta) / DRAG_SENSITIVITY.comboThreshold);\n\n if (optionDelta > 0) {\n const direction = deltaX > 0 ? 1 : -1;\n let newIndex = this.startOptionIndex + (optionDelta * direction);\n\n // Wrap around\n newIndex = ((newIndex % options.length) + options.length) % options.length;\n\n const newValue = options[newIndex];\n WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue);\n this.config.currentValue = newValue;\n this.config.onChange?.(newValue);\n }\n }\n\n /**\n * Handle pointer up - end drag\n */\n private onPointerUp(e: PointerEvent): void {\n if (!this.isDragging) return;\n this.endDrag(e.pointerId);\n Logger.debug(`[DragValue] Ended drag on ${this.config.widgetName}`);\n }\n\n /**\n * Handle pointer cancel - browser cancelled the pointer (e.g., touch scroll)\n */\n private onPointerCancel(e: PointerEvent): void {\n if (!this.isDragging) return;\n this.endDrag(e.pointerId);\n Logger.debug(`[DragValue] Drag cancelled on ${this.config.widgetName}`);\n }\n\n /**\n * Handle lost pointer capture - capture was taken by another element\n */\n private onLostCapture(_e: PointerEvent): void {\n if (!this.isDragging) return;\n this.endDrag();\n Logger.debug(`[DragValue] Lost capture on ${this.config.widgetName}`);\n }\n\n /**\n * Common cleanup for ending drag\n */\n private endDrag(pointerId?: number): void {\n this.isDragging = false;\n this.element.classList.remove('dragging');\n\n // Release pointer capture if we have a pointer ID\n if (pointerId !== undefined) {\n try {\n this.element.releasePointerCapture(pointerId);\n } catch {\n // Ignore if already released\n }\n }\n\n // Remove all listeners\n document.removeEventListener('pointermove', this.boundPointerMove, true);\n document.removeEventListener('pointerup', this.boundPointerUp, true);\n document.removeEventListener('pointercancel', this.boundPointerCancel, true);\n this.element.removeEventListener('lostpointercapture', this.boundLostCapture);\n }\n\n /**\n * Update the current value (call when value changes externally)\n */\n updateValue(value: unknown): void {\n this.config.currentValue = value;\n }\n\n /**\n * Cleanup and remove drag handling\n */\n destroy(): void {\n this.element.classList.remove('draggable', 'dragging');\n\n // Remove indicator\n const indicator = this.element.querySelector('.drag-indicator');\n if (indicator) {\n indicator.remove();\n }\n\n // Remove document listeners if still attached (with capture flag)\n document.removeEventListener('pointermove', this.boundPointerMove, true);\n document.removeEventListener('pointerup', this.boundPointerUp, true);\n document.removeEventListener('pointercancel', this.boundPointerCancel, true);\n this.element.removeEventListener('lostpointercapture', this.boundLostCapture);\n }\n\n /**\n * Check if a widget type supports drag editing\n */\n static isTypeSupported(widgetType: string): boolean {\n const type = widgetType.toLowerCase();\n return [\n 'number', 'int', 'float', 'slider', 'combo'\n ].includes(type);\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file