This is the first iteration of the "multi" number inputs. The behaviour is based on Houdini number inputs
335 lines
9.1 KiB
JavaScript
335 lines
9.1 KiB
JavaScript
// 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()
|
|
}
|
|
}
|