|
|
|
|
|
|
|
|
|
|
|
|
|
function getValidNumber(numberInput) { |
|
let num = |
|
isNaN(numberInput.value) || numberInput.value === '' |
|
? 0 |
|
: parseFloat(numberInput.value) |
|
return num |
|
} |
|
|
|
|
|
|
|
export class NumberInputWidget { |
|
constructor(containerId, numberOfInputs = 1, isDebug = false) { |
|
this.container = document.getElementById(containerId) |
|
this.numberOfInputs = numberOfInputs |
|
this.currentInput = null |
|
|
|
this.threshold = 30 |
|
this.mouseSensitivityMultiplier = 0.05 |
|
this.debug = isDebug |
|
|
|
|
|
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 |
|
} |
|
|
|
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 = [] |
|
|
|
|
|
for (let i = 0; i < this.numberOfInputs; i++) { |
|
const numberInput = document.createElement('input') |
|
numberInput.type = 'number' |
|
numberInput.className = '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' |
|
this.container.appendChild(this.sensitivityMenu) |
|
|
|
|
|
const stepsValues = [0.001, 0.01, 0.1, 1, 10, 100] |
|
stepsValues.forEach((value) => { |
|
const step = document.createElement('div') |
|
step.className = 'step' |
|
step.dataset.step = value |
|
step.textContent = value.toString() |
|
this.sensitivityMenu.appendChild(step) |
|
}) |
|
|
|
this.steps = this.sensitivityMenu.getElementsByClassName('step') |
|
|
|
if (this.debug) { |
|
this.debugContainer = document.createElement('div') |
|
this.debugContainer.id = 'debug-container' |
|
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 |
|
|
|
const horizontalDistanceFromInitial = Math.abs( |
|
event.target.offsetWidth - this.initialMouseX, |
|
) |
|
|
|
|
|
if (horizontalDistanceFromInitial < this.threshold) { |
|
this.thresholdExceeded = false |
|
this.stepLocked = false |
|
this.accumulatedDelta = 0 |
|
} |
|
|
|
|
|
if (!this.stepLocked) { |
|
for (let step of this.steps) { |
|
step.classList.remove('active') |
|
step.classList.remove('locked') |
|
if ( |
|
relativeY >= step.offsetTop && |
|
relativeY <= step.offsetTop + step.offsetHeight |
|
) { |
|
step.classList.add('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) { |
|
|
|
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() |
|
} |
|
} |
|
|