Spaces:
Running
on
Zero
Running
on
Zero
// This is a vanillajs implementation of Houdini's number input widgets. | |
// It basically popup a visual sensitivity slider of steps to use as incr/decr | |
// TODO: Convert it to IWidget | |
// import styles from "./style.module.css"; | |
function getValidNumber(numberInput) { | |
let num = | |
isNaN(numberInput.value) || numberInput.value === '' | |
? 0 | |
: parseFloat(numberInput.value) | |
return num | |
} | |
/** | |
* Number input widgets | |
*/ | |
export class NumberInputWidget { | |
constructor(containerId, numberOfInputs = 1, isDebug = false) { | |
this.container = document.getElementById(containerId) | |
this.numberOfInputs = numberOfInputs | |
this.currentInput = null // Store the currently active input | |
this.threshold = 30 | |
this.mouseSensitivityMultiplier = 0.05 | |
this.debug = isDebug | |
//- states | |
this.initialMouseX | |
this.lastMouseX | |
this.activeStep = 1 | |
this.accumulatedDelta = 0 | |
this.stepLocked = false | |
this.thresholdExceeded = false | |
this.isDragging = false | |
const styleTagId = 'mtb-constant-style' | |
let styleTag = document.head.querySelector(`#${styleTagId}`) | |
if (!styleTag) { | |
styleTag = document.createElement('style') | |
styleTag.type = 'text/css' | |
styleTag.id = styleTagId | |
styleTag.innerHTML = ` | |
.${containerId}{ | |
margin-top: 20px; | |
margin-bottom: 20px; | |
} | |
.sensitivity-menu { | |
display: none; | |
position: absolute; | |
/* Additional styling */ | |
} | |
.sensitivity-menu .step { | |
cursor: pointer; | |
padding: 0.5em; | |
/* Add more styling as needed */ | |
} | |
.sensitivity-menu { | |
font-family: monospace; | |
background: var(--bg-color); | |
border: 1px solid var(--fg-color); | |
/* Highlight for the active step */ | |
} | |
.number-input { | |
background: var(--bg-color); | |
color: var(--fg-color) | |
} | |
.sensitivity-menu .step.active { | |
background-color:var(--drag-text); | |
/* Highlight for the active step */ | |
} | |
.sensitivity-menu .step.locked { | |
background-color: #f00; | |
/* Change to your preferred color for the locked state */ | |
} | |
#debug-container { | |
transform: translateX(50%); | |
width: 50%; | |
text-align: center; | |
font-family: monospace; | |
} | |
` | |
document.head.appendChild(styleTag) | |
} | |
this.createWidgetElements() | |
this.initializeEventListeners() | |
} | |
setLabel(str) { | |
this.label.textContent = str | |
} | |
setValue(...values) { | |
if (values.length !== this.numberInputs.length) { | |
console.error('Number of values does not match the number of inputs.') | |
console.error( | |
`You provided ${values.length} but the input want ${this.numberInputs.length}`, | |
{ values }, | |
) | |
return | |
} | |
// Set each input value | |
this.numberInputs.forEach((input, index) => { | |
input.value = values[index] | |
}) | |
} | |
getValue() { | |
const value = [] | |
this.numberInputs.forEach((input, index) => { | |
value.push(Number.parseFloat(input.value) || 0.0) | |
}) | |
return value | |
} | |
resetValues() { | |
for (const input of numberInputs) { | |
input.value = 0 | |
} | |
this.onChange?.(this.getValue()) | |
} | |
createWidgetElements() { | |
this.label = document.createElement('label') | |
this.label.textContent = 'Control All:' | |
this.label.className = 'widget-label' | |
this.container.appendChild(this.label) | |
this.label.addEventListener('mousedown', (event) => { | |
if (event.button === 1) { | |
this.currentInput = null | |
this.handleMouseDown(event) | |
} | |
}) | |
this.label.addEventListener('contextmenu', (event) => { | |
event.preventDefault() | |
this.resetValues() | |
}) | |
this.numberInputs = [] | |
// create linked inputs | |
for (let i = 0; i < this.numberOfInputs; i++) { | |
const numberInput = document.createElement('input') | |
numberInput.type = 'number' | |
numberInput.className = 'number-input' //styles.numberInput; //"number-input"; | |
numberInput.step = 'any' | |
this.container.appendChild(numberInput) | |
this.numberInputs.push(numberInput) | |
numberInput.addEventListener('mousedown', (event) => { | |
if (event.button === 1) { | |
this.currentInput = numberInput | |
this.handleMouseDown(event) | |
} | |
}) | |
} | |
this.sensitivityMenu = document.createElement('div') | |
this.sensitivityMenu.className = 'sensitivity-menu' //styles.sensitivityMenu; //"sensitivity-menu"; | |
this.container.appendChild(this.sensitivityMenu) | |
// create steps | |
const stepsValues = [0.001, 0.01, 0.1, 1, 10, 100] | |
stepsValues.forEach((value) => { | |
const step = document.createElement('div') | |
step.className = 'step' //styles.step //"step"; | |
step.dataset.step = value | |
step.textContent = value.toString() | |
this.sensitivityMenu.appendChild(step) | |
}) | |
this.steps = this.sensitivityMenu.getElementsByClassName('step') //styles.step) | |
if (this.debug) { | |
this.debugContainer = document.createElement('div') | |
this.debugContainer.id = 'debug-container' //styles.debugContainer //"debugContainer"; | |
document.body.appendChild(this.debugContainer) | |
} | |
} | |
showSensitivityMenu(pageX, pageY) { | |
this.sensitivityMenu.style.display = 'block' | |
this.sensitivityMenu.style.left = `${pageX}px` | |
this.sensitivityMenu.style.top = `${pageY}px` | |
this.initialMouseX = pageX | |
this.lastMouseX = pageX | |
this.isDragging = true | |
this.thresholdExceeded = false | |
this.stepLocked = false | |
this.updateDebugInfo() | |
} | |
updateDebugInfo() { | |
if (this.debug) { | |
this.debugContainer.innerHTML = ` | |
<div>Active Step: ${this.activeStep}</div> | |
<div>Initial Mouse X: ${this.initialMouseX}</div> | |
<div>Last Mouse X: ${this.lastMouseX}</div> | |
<div>Accumulated Delta: ${this.accumulatedDelta}</div> | |
<div>Threshold Exceeded: ${this.thresholdExceeded}</div> | |
<div>Step Locked: ${this.stepLocked}</div> | |
<div>Number Input Value: ${this.currentInput?.value}</div> | |
` | |
} | |
} | |
handleMouseDown(event) { | |
if (event.button === 1) { | |
this.showSensitivityMenu( | |
event.target.offsetWidth, | |
event.target.offsetHeight, | |
) | |
event.preventDefault() | |
} | |
} | |
handleMouseUp(event) { | |
if (event.button === 1) { | |
this.resetWidgetState() | |
} | |
} | |
handleClickOutside(event) { | |
if (event.target !== this.numberInput) { | |
this.resetWidgetState() | |
} | |
} | |
handleMouseMove(event) { | |
if (this.sensitivityMenu.style.display === 'block') { | |
const relativeY = event.pageY - 300 // this.sensitivityMenu.offsetTop | |
const horizontalDistanceFromInitial = Math.abs( | |
event.target.offsetWidth - this.initialMouseX, | |
) | |
// Unlock if the mouse moves back towards the initial position | |
if (horizontalDistanceFromInitial < this.threshold) { | |
this.thresholdExceeded = false | |
this.stepLocked = false | |
this.accumulatedDelta = 0 | |
} | |
// Update step only if it is not locked | |
if (!this.stepLocked) { | |
for (let step of this.steps) { | |
step.classList.remove('active') //styles.active) | |
step.classList.remove('locked') //styles.locked) | |
if ( | |
relativeY >= step.offsetTop && | |
relativeY <= step.offsetTop + step.offsetHeight | |
) { | |
step.classList.add('active') //styles.active) | |
this.setActiveStep(parseFloat(step.dataset.step)) | |
} | |
} | |
} | |
if (this.stepLocked) { | |
this.sensitivityMenu | |
.querySelector('.step.active') | |
?.classList.add('locked') | |
} | |
this.updateStepValue(event.pageX) | |
} | |
} | |
initializeEventListeners() { | |
document.addEventListener('mousemove', (event) => | |
this.handleMouseMove(event), | |
) | |
document.addEventListener('mouseup', (event) => this.handleMouseUp(event)) | |
document.addEventListener('click', (event) => | |
this.handleClickOutside(event), | |
) | |
} | |
setActiveStep(val) { | |
if (this.activeStep !== val) { | |
this.activeStep = val | |
this.stepLocked = false | |
this.accumulatedDelta = 0 | |
this.thresholdExceeded = false | |
} | |
} | |
resetWidgetState() { | |
this.sensitivityMenu.style.display = 'none' | |
this.isDragging = false | |
this.lastMouseX = undefined | |
this.thresholdExceeded = false | |
this.stepLocked = false | |
this.updateDebugInfo() | |
} | |
updateStepValue(mouseX) { | |
if (this.isDragging && this.lastMouseX !== undefined) { | |
const deltaX = mouseX - this.lastMouseX | |
this.accumulatedDelta += deltaX | |
if ( | |
!this.thresholdExceeded && | |
Math.abs(this.accumulatedDelta) > this.threshold | |
) { | |
this.thresholdExceeded = true | |
this.stepLocked = true | |
} | |
if (this.thresholdExceeded && this.stepLocked) { | |
// frequency of value changes | |
if ( | |
Math.abs(this.accumulatedDelta) * this.mouseSensitivityMultiplier >= | |
1 | |
) { | |
const valueChange = Math.sign(this.accumulatedDelta) * this.activeStep | |
if (this.currentInput) { | |
this.currentInput.value = | |
getValidNumber(this.currentInput) + valueChange | |
this.onChange?.(this.getValue()) | |
} else { | |
this.numberInputs.forEach((input) => { | |
input.value = getValidNumber(input) + valueChange | |
}) | |
} | |
this.accumulatedDelta = 0 | |
} | |
} | |
this.lastMouseX = mouseX | |
} | |
this.updateDebugInfo() | |
} | |
} | |